├── .editorconfig ├── .gitignore ├── .travis.yml ├── .travis_scripts └── publishSnapshots.sh ├── LICENSE ├── README.md ├── bson ├── build.sbt └── src │ ├── main │ └── scala │ │ ├── dao │ │ ├── BsonDao.scala │ │ ├── BsonDaoBuilder.scala │ │ └── BsonFileDao.scala │ │ ├── dsl │ │ ├── BsonDsl.scala │ │ └── criteria │ │ │ ├── Expression.scala │ │ │ ├── Term.scala │ │ │ ├── Typed.scala │ │ │ ├── Untyped.scala │ │ │ └── ValueBuilder.scala │ │ └── fixtures │ │ └── BsonFixtures.scala │ └── test │ ├── resources │ └── bson │ │ ├── events.conf │ │ └── persons.conf │ └── scala │ ├── dao │ ├── BsonFileDaoSpec.scala │ ├── CustomIdBsonDao.scala │ ├── CustomIdBsonDaoSpec.scala │ ├── DummyBsonDao.scala │ ├── DummyBsonDaoSpec.scala │ ├── DynamicBsonDaoSpec.scala │ ├── EventBsonDao.scala │ ├── MapModelBsonDao.scala │ ├── MapModelBsonDaoSpec.scala │ ├── PersonBsonDao.scala │ ├── TemporalModelDao.scala │ └── TemporalModelDaoSpec.scala │ ├── dsl │ ├── BsonDslSpec.scala │ └── criteria │ │ ├── UntypedCriteriaSpec.scala │ │ └── UntypedWhereSpec.scala │ ├── fixtures │ └── BsonFixturesSpec.scala │ └── model │ ├── CustomIdModel.scala │ ├── DummyModel.scala │ ├── Event.scala │ ├── MapModel.scala │ ├── Person.scala │ └── TemporalModel.scala ├── build.sbt ├── core ├── build.sbt └── src │ ├── main │ └── scala │ │ ├── Implicits.scala │ │ ├── bson.scala │ │ ├── dao │ │ ├── Dao.scala │ │ ├── FileDao.scala │ │ ├── Handlers.scala │ │ └── LifeCycle.scala │ │ ├── fixtures │ │ └── Fixtures.scala │ │ └── util │ │ └── Logger.scala │ └── test │ ├── resources │ ├── logback.xml │ └── whyfp90.pdf │ └── scala │ ├── dao │ └── MongoContext.scala │ └── util │ ├── ColoredLevel.scala │ ├── Colors.scala │ └── Misc.scala ├── guide ├── bsondao.md ├── criteria.md ├── dsl.md └── jsondao.md ├── json ├── build.sbt └── src │ ├── main │ └── scala │ │ ├── dao │ │ ├── JsonDao.scala │ │ ├── JsonDaoBuilder.scala │ │ └── JsonFileDao.scala │ │ ├── dsl │ │ └── JsonDsl.scala │ │ └── fixtures │ │ └── JsonFixtures.scala │ └── test │ ├── resources │ └── json │ │ ├── events.conf │ │ └── persons.conf │ └── scala │ ├── dao │ ├── CustomIdJsonDao.scala │ ├── CustomIdJsonDaoSpec.scala │ ├── DummyJsonDao.scala │ ├── DummyJsonDaoSpec.scala │ ├── DynamicJsonDaoSpec.scala │ ├── EventJsonDao.scala │ ├── JsonFileDaoSpec.scala │ ├── PersonJsonDao.scala │ ├── TemporalModelJsonDao.scala │ └── TemporalModelJsonDaoSpec.scala │ ├── dsl │ └── JsonDslSpec.scala │ ├── fixtures │ └── JsonFixturesSpec.scala │ └── model │ ├── CustomIdModel.scala │ ├── DummyModel.scala │ ├── Event.scala │ ├── MapModel.scala │ ├── Person.scala │ └── TemporalModel.scala ├── project ├── Colors.scala ├── Common.scala ├── Travis.scala ├── build.properties └── plugins.sbt └── samples ├── build.sbt └── src └── test └── scala └── dsl └── criteria └── TypedCriteriaSpec.scala /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | 9 | [*.{scala,sbt}] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [*.xml] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [*.json] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | [*.conf] 22 | indent_style = space 23 | indent_size = 4 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all dotfiles... 2 | .* 3 | # except for .gitignore and .travis_scripts 4 | !.gitignore 5 | !.travis_scripts 6 | 7 | *.log 8 | 9 | # sbt specific 10 | dist/* 11 | target/ 12 | lib_managed/ 13 | src_managed/ 14 | project/boot/ 15 | project/plugins/project/ 16 | 17 | *.sublime-* 18 | *.iml 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | script: sbt ++$TRAVIS_SCALA_VERSION 'test-only' 3 | scala: 4 | - 2.11.6 5 | jdk: 6 | - oraclejdk8 7 | before_install: 8 | - sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 9 | - echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | 10 | sudo tee /etc/apt/sources.list.d/mongodb.list 11 | - sudo apt-get update 12 | - sudo apt-get install mongodb-org-server 13 | env: 14 | global: 15 | - secure: a+TF5tTj1Nb6knida4xcf/IWVF5gF29PQ2hg0XTSM/2JZ0qQEl+xm01P1K+rzO/nO8bQ5G4BXPhNYv7ReS9otKvdPf2ya6iOjYJ1ZPA1QyUSulzq5DUHEWWBww+vV6HR0tAEtLSFqx05u0Igx9f33/kxtzrabgm/zQqGFu+B+lY= 16 | - secure: RCIIIn5IERegftnUMww+azwWxDTe8pDy256LbLO/7n8WN06DGVZPeZmQY33WQdTg4Qz/Mri1QlAF8YnG+TeNKooXHlkFXlBIq00bM6Dab9QRvbZEll2NV5agZ112kcL0IOL7Yw5cY3knjsWpT3IttChMopLxPGGV1YzvExW4B5Y= 17 | after_success: 18 | - .travis_scripts/publishSnapshots.sh 19 | -------------------------------------------------------------------------------- /.travis_scripts/publishSnapshots.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo -e "Starting publish to Sonatype...\n" 4 | 5 | sbt ";project bson;publishSnapshotsFromTravis ;project core;publishSnapshotsFromTravis ;project json;publishSnapshotsFromTravis" 6 | RETVAL=$? 7 | 8 | if [ $RETVAL -eq 0 ]; then 9 | echo "Snapshots successfully published" 10 | else 11 | echo "Error while publishing snapshots" 12 | exit 1 13 | fi 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReactiveMongo Extensions 2 | 3 | The goal of *ReactiveMongo Extensions* is to provide all the necessary tools for ReactiveMongo other than the core functionality. 4 | 5 | > **Since ReactiveMongo 0.11, some useful functions are available directly in the [driver](../ReactiveMongo), so these extensions are not updated, and it's recommended to directly use the driver.** 6 | > Some third party projects like [ReactiveMongo-Criteria](https://github.com/osxhacker/ReactiveMongo-Criteria) can also be used. 7 | 8 | ## Introduction 9 | 10 | **ReactiveMongo Extensions** comes as 2 separate packages which are `reactivemongo-extensions-bson` and `reactivemongo-extensions-json`. 11 | *reactivemongo-extensions-bson* package targets ReactiveMongo, while *reactivemongo-extensions-json* targets Play-ReactiveMongo. 12 | 13 | #### DAOs to Serve You 14 | 15 | DAOs provide an abstraction layer on top of ReactiveMongo adding higher level APIs like findOne, findById, count, foreach, fold, etc. 16 | *ReactiveMongo Extensions* currently provides two DAO types for collections of documents: `reactivemongo.extensions.dao.BsonDao` for `BSONCollection` and `reactivemongo.extensions.dao.JsonDao` for `JSONCollection`. There are also two other DAO types operating on GridFS: `reactivemongo.extensions.dao.BsonFileDao` and `reactivemongo.extensions.dao.JsonFileDao`. 17 | 18 | You will need to define a DAO for each of your models(case classes). 19 | 20 | Below is a sample model. 21 | 22 | ```scala 23 | import reactivemongo.bson._ 24 | import reactivemongo.extensions.dao.Handlers._ 25 | 26 | case class Person( 27 | _id: BSONObjectID = BSONObjectID.generate, 28 | name: String, 29 | surname: String, 30 | age: Int) 31 | 32 | object Person { 33 | implicit val personHandler = Macros.handler[Person] 34 | } 35 | ``` 36 | 37 | To define a BsonDao for the Person model you just need to extend BsonDao. 38 | 39 | ```scala 40 | import reactivemongo.api.{ MongoDriver, DB } 41 | import reactivemongo.bson.BSONObjectID 42 | import reactivemongo.bson.DefaultBSONHandlers._ 43 | import reactivemongo.extensions.dao.BsonDao 44 | import scala.concurrent.ExecutionContext.Implicits.global 45 | 46 | object MongoContext { 47 | val driver = new MongoDriver 48 | val connection = driver.connection(List("localhost")) 49 | def db: DB = connection("reactivemongo-extensions") 50 | } 51 | 52 | object PersonDao extends BsonDao[Person, BSONObjectID](MongoContext.db, "persons") 53 | ``` 54 | 55 | From now on you can insert a Person instance, find it by id, find a random person or etc. 56 | 57 | ```scala 58 | val person1 = Person(name = "foo", surname = "bar", age = 16) 59 | val person2 = Person(name = "fehmi can", surname = "saglam", age = 32) 60 | val person3 = Person(name = "ali", surname = "veli", age = 64) 61 | 62 | PersonDao.insert(person1) 63 | PersonDao.insert(Seq(person2, person3)) 64 | 65 | PersonDao.findById(person1._id) 66 | PersonDao.findRandom(BSONDocument("age" -> BSONDocument("$ne" -> 16))) 67 | ``` 68 | 69 | Read more about BsonDao [here](guide/bsondao.md) and JsonDao [here](guide/jsondao.md). 70 | 71 | #### Query DSL for Easy Query Construction 72 | 73 | There are also DSL helpers for each DAO type, which are `reactivemongo.extensions.dsl.BsonDsl` and `reactivemongo.extensions.json.dsl.JsonDsl`. 74 | DSL helpers provide utilities to easily construct JSON or BSON queries. 75 | 76 | By mixing in or importing BsonDsl you could write the query above like this: 77 | 78 | ```scala 79 | import reactivemongo.extensions.dsl.BsonDsl._ 80 | 81 | PersonDao.findRandom("age" $gt 16 $lt 32) 82 | ``` 83 | 84 | Read more about Query DSL [here](guide/dsl.md). 85 | 86 | #### Criteria DSL 87 | 88 | > An alternative providing such DSL extension can be found as a [third party project](https://github.com/osxhacker/ReactiveMongo-Criteria). 89 | 90 | The Criteria DSL provides the ablity to formulate queries thusly: 91 | 92 | ```scala 93 | // Using an Untyped.criteria 94 | { 95 | import Untyped._ 96 | 97 | // The MongoDB properties referenced are not enforced by the compiler 98 | // to belong to any particular type. This is what is meant by "Untyped". 99 | val adhoc = criteria.firstName === "Jack" && criteria.age >= 18; 100 | val cursor = collection.find(adhoc).cursor[BSONDocument]; 101 | } 102 | 103 | { 104 | // Using a Typed criteria which restricts properties to the 105 | // given type. 106 | import Typed._ 107 | 108 | case class ExampleDocument (aProperty : String, another : Int) 109 | 110 | val byKnownProperties = criteria[ExampleDocument].aProperty =~ "^[A-Z]\\w+" && 111 | criteria[ExampleDocument].another > 0; 112 | val cursor = collection.find(byKnownProperties).cursor[BSONDocument]; 113 | } 114 | ``` 115 | 116 | Read more about Criteria DSL [here](guide/criteria.md). 117 | 118 | #### Model Life Cyle 119 | 120 | By defining a life cycle object, one can preprocess all models before being persisted or perform specific actions after life cycle events. This can be useful for updating temporal fields on all model instances before persisting. 121 | 122 | ```scala 123 | import reactivemongo.bson._ 124 | import reactivemongo.extensions.dao.LifeCycle 125 | import reactivemongo.extensions.dao.Handlers._ 126 | import reactivemongo.extensions.util.Logger 127 | import org.joda.time.DateTime 128 | 129 | case class TemporalModel( 130 | _id: BSONObjectID = BSONObjectID.generate, 131 | name: String, 132 | surname: String, 133 | createdAt: DateTime = DateTime.now, 134 | updatedAt: DateTime = DateTime.now) 135 | 136 | object TemporalModel { 137 | implicit val temporalModelFormat = Macros.handler[TemporalModel] 138 | 139 | implicit object TemporalModelLifeCycle extends LifeCycle[TemporalModel, BSONObjectID] { 140 | def prePersist(model: TemporalModel): TemporalModel = { 141 | Logger.debug(s"prePersist $model") 142 | model.copy(updatedAt = DateTime.now) 143 | } 144 | def postPersist(model: TemporalModel): Unit = { 145 | Logger.debug(s"postPersist $model") 146 | } 147 | def preRemove(id: BSONObjectID): Unit = { 148 | Logger.debug(s"preRemove $id") 149 | } 150 | def postRemove(id: BSONObjectID): Unit = { 151 | Logger.debug(s"postRemove $id") 152 | } 153 | def ensuredIndexes(): Unit = { 154 | Logger.debug("ensuredIndexes") 155 | } 156 | } 157 | } 158 | ``` 159 | 160 | #### Auto Indexes 161 | 162 | ReactiveMongo Extensions support auto indexes which ensures indexes on DAO load. 163 | 164 | ```scala 165 | object PersonDao extends { 166 | override val autoIndexes = Seq( 167 | Index(Seq("name" -> IndexType.Ascending), unique = true, background = true), 168 | Index(Seq("age" -> IndexType.Ascending), background = true) 169 | ) 170 | } with BsonDao[Person, BSONObjectID](MongoContext.db, "persons") 171 | 172 | ``` 173 | 174 | #### Default Write Concern 175 | 176 | You can override writeConcern in your DAO definition which defaults to GetLastError(). 177 | 178 | ```scala 179 | object PersonDao extends BsonDao[Person, BSONObjectID](MongoContext.db, "persons") { 180 | override def defaultWriteConcern = GetLastError(j = true) 181 | } 182 | ``` 183 | 184 | #### Fixtures for easy data loading 185 | 186 | You can define your fixtures using HOCON. Lexical scopes are supported in addition to HOCON spec. 187 | 188 | **persons.conf** 189 | 190 | _predef { 191 | country: TC 192 | } 193 | 194 | # "persons" collection 195 | persons { 196 | person1 { 197 | _id: _id_person1 198 | name: Ali 199 | surname: Veli 200 | fullname: ${name} ${surname} 201 | age: 32 202 | salary: 999.85 203 | time: 12345678900 204 | country: ${_predef.country} 205 | } 206 | 207 | person2 { 208 | _id: _id_person2 209 | name: Haydar 210 | surname: Cabbar 211 | fullname: ${name} ${surname} 212 | age: ${person1.age} 213 | salary: { "$double": 1000.0 } 214 | time: 12345678999 215 | country: ${_predef.country} 216 | } 217 | } 218 | 219 | 220 | **events.conf** 221 | 222 | # Predefined reusable values 223 | _predef { 224 | location: { 225 | city: Ankara 226 | place: Salon 227 | } 228 | } 229 | 230 | # "events" collection 231 | events { 232 | event1 { 233 | _id: _id_event1 234 | title: Developer workshop 235 | organizer: ${persons.person1.fullname} 236 | location: ${_predef.location} 237 | } 238 | } 239 | 240 | After defining your fixtures you can load them using `BsonFixtures` or `JsonFixtures`. 241 | 242 | 243 | ```scala 244 | import reactivemongo.extensions.bson.fixtures.BsonFixtures 245 | 246 | BsonFixtures(db).load("persons.conf", "events.conf") 247 | ``` 248 | 249 | #### ```~``` Operator for the Happy Path 250 | 251 | While composing futures with for comprehensions, handling option values can be cumbersome. 252 | ```~``` operator converts a ```Future[Option[T]]``` to ```Future[T]```. It throws a *java.util.NoSuchElementException* if the Option is None. 253 | Then you can check the exception in ```Future.recover```. 254 | 255 | ```scala 256 | import reactivemongo.extensions.Implicits._ 257 | 258 | (for { 259 | model1 <- ~dao.findOne("none" $eq "unknown") 260 | model2 <- ~dao.findOne("none" $eq "unknown") 261 | result <- compute(model1, model2) 262 | } yield result) recover { 263 | case ex: java.util.NoSuchElementException => 264 | println("Option is None") 265 | throw ex 266 | } 267 | 268 | ``` 269 | 270 | ## Using ReactiveMongo Extensions in your project 271 | 272 | The general format is that release a.b.c.d is compatible with ReactiveMongo a.b.c. 273 | Current version matrix is below: 274 | 275 | | reactivemongo-extensions-bson | Target ReactiveMongo version | 276 | |----------------------------------|------------------------------| 277 | | 0.10.0.0-SNAPSHOT | 0.10.0 | 278 | 279 | | reactivemongo-extensions-json | Target Play-ReactiveMongo version | 280 | |----------------------------------|-----------------------------------| 281 | | 0.10.0.0-SNAPSHOT | 0.10.2 | 282 | 283 | Note: Only available for scala 2.10. 284 | 285 | If you use SBT, you just have to edit build.sbt and add the following: 286 | 287 | ```scala 288 | resolvers += "Sonatype Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/" 289 | 290 | libraryDependencies ++= Seq( 291 | "org.reactivemongo" %% "reactivemongo-extensions-bson" % "0.10.0.0-SNAPSHOT" 292 | ) 293 | ``` 294 | 295 | or 296 | 297 | ```scala 298 | resolvers += "Sonatype Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/" 299 | 300 | libraryDependencies ++= Seq( 301 | "org.reactivemongo" %% "reactivemongo-extensions-json" % "0.10.0.0-SNAPSHOT" 302 | ) 303 | ``` 304 | -------------------------------------------------------------------------------- /bson/build.sbt: -------------------------------------------------------------------------------- 1 | import Common.{ playVersion, playReactiveMongoVersion } 2 | 3 | name := "reactivemongo-extensions-bson" 4 | 5 | libraryDependencies ++= Seq( 6 | "org.reactivemongo" %% "play2-reactivemongo" % playReactiveMongoVersion, 7 | "com.typesafe.play" %% "play-json" % playVersion % "provided" 8 | ) 9 | -------------------------------------------------------------------------------- /bson/src/main/scala/dao/BsonDaoBuilder.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import reactivemongo.api.DB 20 | import reactivemongo.bson.{ 21 | BSONDocumentWriter, 22 | BSONDocumentReader, 23 | BSONReader, 24 | BSONValue, 25 | BSONWriter 26 | } 27 | 28 | import scala.concurrent.ExecutionContext 29 | 30 | class BsonDaoBuilder[Model, ID](db: => DB) { 31 | def apply(collectionName: String)( 32 | implicit modelReader: BSONDocumentReader[Model], 33 | modelWriter: BSONDocumentWriter[Model], 34 | idWriter: BSONWriter[ID, _ <: BSONValue], 35 | idReader: BSONReader[_ <: BSONValue, ID], 36 | lifeCycle: LifeCycle[Model, ID] = new ReflexiveLifeCycle[Model, ID], 37 | ec: ExecutionContext): BsonDao[Model, ID] = { 38 | BsonDao(db, collectionName) 39 | } 40 | } 41 | 42 | object BsonDaoBuilder { 43 | def apply[Model, ID](db: => DB): BsonDaoBuilder[Model, ID] = { 44 | new BsonDaoBuilder[Model, ID](db) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /bson/src/main/scala/dao/BsonFileDao.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import reactivemongo.bson.{ BSONWriter, BSONDocument, BSONValue, Producer } 20 | import reactivemongo.api.{ DBMetaCommands, DB } 21 | import reactivemongo.api.gridfs.IdProducer 22 | import reactivemongo.extensions.dao.FileDao.ReadFileWrapper 23 | import scala.concurrent.ExecutionContext 24 | 25 | abstract class BsonFileDao[Id <: BSONValue: IdProducer](db: => DB with DBMetaCommands, collectionName: String) extends FileDao[Id, BSONDocument](db, collectionName) { 26 | 27 | def findById(id: Id)(implicit ec: ExecutionContext): ReadFileWrapper = { 28 | findOne(BSONDocument("_id" -> id)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bson/src/main/scala/dsl/criteria/Expression.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Steve Vickers 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package reactivemongo.extensions.dsl.criteria 17 | 18 | import scala.language.{ 19 | dynamics, 20 | implicitConversions 21 | } 22 | 23 | import reactivemongo.bson._ 24 | 25 | /** 26 | * The '''Expression''' type defines a recursive propositional abstract 27 | * syntax tree central to the MongoDB embedded domain-specific language (EDSL). 28 | * It is the main abstraction used to provide the EDSL and results in being 29 | * able to write: 30 | * 31 | * {{{ 32 | * import Untyped._ 33 | * 34 | * val edslQuery = criteria.first < 10 && ( 35 | * criteria.second >= 20.0 || criteria.second.in (0.0, 1.0) 36 | * ); 37 | * }}} 38 | * 39 | * And have that equivalent to this filter: 40 | * 41 | * {{{ 42 | * val bsonQuery = BSONDocument ( 43 | * "$and" -> 44 | * BSONArray ( 45 | * BSONDocument ( 46 | * "first" -> BSONDocument ("$lt" -> BSONInteger (10)) 47 | * ), 48 | * BSONDocument ( 49 | * "$or" -> 50 | * BSONArray ( 51 | * BSONDocument ( 52 | * "second" -> BSONDocument ("$gte" -> BSONDouble (20.0)) 53 | * ), 54 | * BSONDocument ( 55 | * "second" -> 56 | * BSONDocument ( 57 | * "$in" -> BSONArray (BSONDouble (0.0), BSONDouble (1.0)) 58 | * ) 59 | * ) 60 | * ) 61 | * ) 62 | * ) 63 | * ); 64 | * }}} 65 | * 66 | * @author svickers 67 | * 68 | */ 69 | case class Expression(name: Option[String], element: BSONElement) { 70 | /// Class Imports 71 | import Expression._ 72 | 73 | /** 74 | * The logical negation operator attempts to invert this '''Expression''' 75 | * by using complimentary operators if possible, falling back to the 76 | * general-case wrapping in a `$not` operator. 77 | */ 78 | def unary_! : Expression = 79 | this match { 80 | case Expression(Some(term), ("$in", vals)) => 81 | Expression(term, ("$nin", vals)); 82 | 83 | case Expression(Some(term), ("$nin", vals)) => 84 | Expression(term, ("$in", vals)); 85 | 86 | case Expression(Some(term), ("$ne", vals)) => 87 | Expression(term, (term, vals)); 88 | 89 | case Expression(Some(term), (field, vals)) if (field == term) => 90 | Expression(term, ("$ne", vals)); 91 | 92 | case Expression(None, ("$nor", vals)) => 93 | Expression(None, ("$or" -> vals)); 94 | 95 | case Expression(None, ("$or", vals)) => 96 | Expression(None, ("$nor" -> vals)); 97 | 98 | case Expression(Some("$not"), el) => 99 | Expression(None, el); 100 | 101 | case Expression(Some(n), _) => 102 | Expression(Some("$not"), (n -> BSONDocument(element))); 103 | 104 | case Expression(None, el) => 105 | Expression(Some("$not"), el); 106 | } 107 | 108 | /** 109 | * Conjunction: ''AND''. 110 | */ 111 | def &&(rhs: Expression): Expression = combine("$and", rhs); 112 | 113 | /** 114 | * Negation of conjunction: ''NOR''. 115 | */ 116 | def !&&(rhs: Expression): Expression = combine("$nor", rhs); 117 | 118 | /** 119 | * Disjunction: ''OR''. 120 | */ 121 | def ||(rhs: Expression): Expression = combine("$or", rhs); 122 | 123 | /** 124 | * The isEmpty method reports as to whether or not this '''Expression''' 125 | * has neither a `name` nor an assigned value. 126 | */ 127 | def isEmpty: Boolean = name.isEmpty && element._1.isEmpty; 128 | 129 | private def combine(op: String, rhs: Expression): Expression = 130 | if (rhs.isEmpty) 131 | this; 132 | else 133 | element match { 134 | case (`op`, arr: BSONArray) => 135 | Expression( 136 | None, 137 | (op, arr ++ BSONArray(toBSONDocument(rhs))) 138 | ); 139 | 140 | case ("", _) => 141 | rhs; 142 | 143 | case _ => 144 | Expression( 145 | None, 146 | ( 147 | op -> BSONArray(toBSONDocument(this), 148 | toBSONDocument(rhs)) 149 | )); 150 | } 151 | } 152 | 153 | object Expression { 154 | /** 155 | * The empty property is provided so that ''monoid'' definitions for 156 | * '''Expression''' can be easily provided. 157 | */ 158 | val empty = new Expression(None, "" -> BSONDocument.empty); 159 | 160 | /** 161 | * The apply method provides functional-style creation syntax for 162 | * [[reactivemongo.extensions.dsl.criteria.Expression]] instances. 163 | */ 164 | def apply(name: String, element: BSONElement): Expression = 165 | new Expression(Some(name), element); 166 | 167 | /// Implicit Conversions 168 | implicit object ExpressionWriter extends BSONDocumentWriter[Expression] { 169 | override def write(expr: Expression): BSONDocument = 170 | toBSONDocument(expr); 171 | } 172 | 173 | implicit def toBSONDocument(expr: Expression): BSONDocument = 174 | expr match { 175 | case Expression(Some(name), (field, element)) if (name == field) => 176 | BSONDocument(field -> element); 177 | 178 | case Expression(Some(name), element) => 179 | BSONDocument(name -> BSONDocument(element)); 180 | 181 | case Expression(None, ("", _)) => 182 | BSONDocument.empty; 183 | 184 | case Expression(None, element) => 185 | BSONDocument(element); 186 | } 187 | 188 | implicit def toBSONElement(expr: Expression): BSONElement = 189 | expr.element; 190 | } 191 | 192 | -------------------------------------------------------------------------------- /bson/src/main/scala/dsl/criteria/Term.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Steve Vickers 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package reactivemongo.extensions.dsl.criteria 17 | 18 | import scala.language.dynamics 19 | 20 | import reactivemongo.bson._ 21 | 22 | /** 23 | * A '''Term'' instance reifies the use of a MongoDB document field, both 24 | * top-level or nested. Operators common to all ''T'' types are defined here 25 | * with type-specific ones provided in the companion object below. 26 | * 27 | * @author svickers 28 | * 29 | */ 30 | case class Term[T](`_term$name`: String) 31 | extends Dynamic { 32 | /** 33 | * Logical equality. 34 | */ 35 | def ===[U <: T: ValueBuilder](rhs: U): Expression = 36 | Expression( 37 | `_term$name`, 38 | `_term$name` -> implicitly[ValueBuilder[U]].bson(rhs) 39 | ); 40 | 41 | /** 42 | * Logical equality. 43 | */ 44 | def @==[U <: T: ValueBuilder](rhs: U): Expression = 45 | ===[U](rhs); 46 | 47 | /** 48 | * Logical inequality: '''$ne'''. 49 | */ 50 | def <>[U <: T: ValueBuilder](rhs: U): Expression = 51 | Expression( 52 | `_term$name`, 53 | "$ne" -> implicitly[ValueBuilder[U]].bson(rhs) 54 | ); 55 | 56 | /** 57 | * Logical inequality: '''$ne'''. 58 | */ 59 | def =/=[U <: T: ValueBuilder](rhs: U): Expression = 60 | <>[U](rhs); 61 | 62 | /** 63 | * Logical inequality: '''$ne'''. 64 | */ 65 | def !==[U <: T: ValueBuilder](rhs: U): Expression = 66 | <>[U](rhs); 67 | 68 | /** 69 | * Less-than comparison: '''$lt'''. 70 | */ 71 | def <[U <: T: ValueBuilder](rhs: U): Expression = 72 | Expression( 73 | `_term$name`, 74 | "$lt" -> implicitly[ValueBuilder[U]].bson(rhs) 75 | ); 76 | 77 | /** 78 | * Less-than or equal comparison: '''$lte'''. 79 | */ 80 | def <=[U <: T: ValueBuilder](rhs: U): Expression = 81 | Expression( 82 | `_term$name`, 83 | "$lte" -> implicitly[ValueBuilder[U]].bson(rhs) 84 | ); 85 | 86 | /** 87 | * Greater-than comparison: '''$gt'''. 88 | */ 89 | def >[U <: T: ValueBuilder](rhs: U): Expression = 90 | Expression( 91 | `_term$name`, 92 | "$gt" -> implicitly[ValueBuilder[U]].bson(rhs) 93 | ); 94 | 95 | /** 96 | * Greater-than or equal comparison: '''$gte'''. 97 | */ 98 | def >=[U <: T: ValueBuilder](rhs: U): Expression = 99 | Expression( 100 | `_term$name`, 101 | "$gte" -> implicitly[ValueBuilder[U]].bson(rhs) 102 | ); 103 | 104 | /** 105 | * Field existence: '''$exists'''. 106 | */ 107 | def exists: Expression = 108 | Expression(`_term$name`, "$exists" -> BSONBoolean(true)); 109 | 110 | /** 111 | * Field value equals one of the '''values''': '''$in'''. 112 | */ 113 | def in[U <: T: ValueBuilder](values: Traversable[U])(implicit B: ValueBuilder[U]): Expression = 114 | Expression(`_term$name`, "$in" -> BSONArray(values map (B.bson))); 115 | 116 | /** 117 | * Field value equals either '''head''' or one of the (optional) '''tail''' values: '''$in'''. 118 | */ 119 | def in[U <: T: ValueBuilder](head: U, tail: U*)(implicit B: ValueBuilder[U]): Expression = 120 | Expression( 121 | `_term$name`, 122 | "$in" -> BSONArray(Seq(B.bson(head)) ++ tail.map(B.bson)) 123 | ); 124 | 125 | def selectDynamic[U](field: String): Term[U] = 126 | Term[U](`_term$name` + "." + field); 127 | } 128 | 129 | object Term { 130 | /// Class Types 131 | /** 132 | * The '''CollectionTermOps''' `implicit` provides EDSL functionality to 133 | * `Seq` [[reactivemongo.extensions.dsl.criteria.Term]]s only. 134 | */ 135 | implicit class CollectionTermOps[T](val term: Term[Seq[T]]) 136 | extends AnyVal { 137 | def all(values: Traversable[T])(implicit B: ValueBuilder[T]): Expression = 138 | Expression( 139 | term.`_term$name`, 140 | "$all" -> BSONArray(values map (B.bson)) 141 | ); 142 | } 143 | 144 | /** 145 | * The '''StringTermOps''' `implicit` enriches 146 | * [[reactivemongo.extensions.dsl.criteria.Term]]s for `String`-only operations. 147 | */ 148 | implicit class StringTermOps[T >: String](val term: Term[T]) 149 | extends AnyVal { 150 | def =~(re: String): Expression = 151 | Expression(term.`_term$name`, "$regex" -> BSONRegex(re, "")); 152 | 153 | def !~(re: String): Expression = 154 | Expression( 155 | term.`_term$name`, 156 | "$not" -> BSONDocument("$regex" -> BSONRegex(re, "")) 157 | ); 158 | } 159 | } 160 | 161 | -------------------------------------------------------------------------------- /bson/src/main/scala/dsl/criteria/Typed.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Steve Vickers 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * Created on: Jun 22, 2014 17 | */ 18 | package reactivemongo.extensions.dsl.criteria 19 | 20 | import scala.language.dynamics 21 | import scala.language.experimental.macros 22 | import scala.reflect.macros.Context 23 | import scala.reflect.runtime.universe._ 24 | 25 | /** 26 | * The '''Typed''' `object` provides the ability to ''lift'' an arbitrary type 27 | * `T` into the [[reactivemongo.extensions.dsl.criteria]] world. Each property 28 | * is represented as a [[reactivemongo.extensions.dsl.criteria.Term]]. 29 | * 30 | * 31 | * @author svickers 32 | * 33 | */ 34 | object Typed { 35 | /// Class Types 36 | class PropertyAccess[T] extends Dynamic { 37 | def selectDynamic(property: String) = macro PropertyAccess.select[T] 38 | } 39 | 40 | object PropertyAccess { 41 | def select[T: c.WeakTypeTag](c: Context)(property: c.Expr[String]) = { 42 | import c.universe._ 43 | import c.universe.typeOf 44 | import c.mirror._ 45 | 46 | val tree = (c.prefix.tree, property.tree) match { 47 | case ( 48 | TypeApply( 49 | Select(_, _), 50 | List(parentType) 51 | ), 52 | st @ Literal(Constant(name: String)) 53 | ) => 54 | val accessor = parentType.tpe.member(newTermName(name)) orElse { 55 | c.abort( 56 | c.enclosingPosition, 57 | s"$name is not a member of ${parentType.tpe}" 58 | ); 59 | } 60 | 61 | Apply( 62 | Select( 63 | New(TypeTree(typeOf[Term[Any]])), 64 | nme.CONSTRUCTOR 65 | ), 66 | List(st) 67 | ); 68 | 69 | case other => 70 | c.abort(c.enclosingPosition, s"only property access is supported: $other"); 71 | } 72 | 73 | c.Expr[Any](tree); 74 | } 75 | } 76 | 77 | /** 78 | * The criteria method produces a type which enforces the existence of 79 | * property names within ''T''. 80 | */ 81 | def criteria[T] = new PropertyAccess[T]; 82 | } 83 | 84 | -------------------------------------------------------------------------------- /bson/src/main/scala/dsl/criteria/Untyped.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Steve Vickers 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package reactivemongo.extensions.dsl.criteria 17 | 18 | import scala.language.dynamics 19 | 20 | /** 21 | * The '''Untyped''' type defines the behaviour expected of queries where the 22 | * MongoDB document may not correspond to a Scala type known to the system 23 | * using this abstraction. 24 | */ 25 | sealed trait Untyped 26 | extends Dynamic { 27 | def selectDynamic(field: String): Term[Any] = 28 | Term[Any](field); 29 | } 30 | 31 | object Untyped { 32 | /** 33 | * The criteria property is a ''factory'' of '''Untyped''' instances. 34 | */ 35 | val criteria = new Untyped {}; 36 | 37 | def where(block: (Untyped) => Expression): Expression = 38 | block(criteria); 39 | 40 | def where(block: (Untyped, Untyped) => Expression): Expression = 41 | block(criteria, criteria); 42 | 43 | def where(block: (Untyped, Untyped, Untyped) => Expression): Expression = 44 | block(criteria, criteria, criteria); 45 | 46 | def where(block: (Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 47 | block(criteria, criteria, criteria, criteria); 48 | 49 | def where(block: (Untyped, Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 50 | block(criteria, criteria, criteria, criteria, criteria); 51 | 52 | def where(block: (Untyped, Untyped, Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 53 | block(criteria, criteria, criteria, criteria, criteria, criteria); 54 | 55 | def where(block: (Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 56 | block(criteria, criteria, criteria, criteria, criteria, criteria, criteria); 57 | 58 | def where(block: (Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 59 | block(criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria); 60 | 61 | def where(block: (Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 62 | block(criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria); 63 | 64 | def where(block: (Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 65 | block(criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria); 66 | 67 | def where(block: (Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 68 | block(criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria); 69 | 70 | def where(block: (Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 71 | block(criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria); 72 | 73 | def where(block: (Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 74 | block(criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria); 75 | 76 | def where(block: (Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 77 | block(criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria); 78 | 79 | def where(block: (Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 80 | block(criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria); 81 | 82 | def where(block: (Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 83 | block(criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria); 84 | 85 | def where(block: (Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 86 | block(criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria); 87 | 88 | def where(block: (Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 89 | block(criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria); 90 | 91 | def where(block: (Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 92 | block(criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria); 93 | 94 | def where(block: (Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 95 | block(criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria); 96 | 97 | def where(block: (Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 98 | block(criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria); 99 | 100 | def where(block: (Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped, Untyped) => Expression): Expression = 101 | block(criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria, criteria); 102 | } 103 | 104 | -------------------------------------------------------------------------------- /bson/src/main/scala/dsl/criteria/ValueBuilder.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Steve Vickers 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package reactivemongo.extensions.dsl.criteria 17 | 18 | import scala.language.implicitConversions 19 | 20 | import reactivemongo.bson._ 21 | 22 | /** 23 | * The '''ValueBuilder'' type is a model of the ''type class'' pattern used to 24 | * produce a ''T''-specific [[reactivemongo.bson.BSONValue]] instance. 25 | * 26 | * @author svickers 27 | * 28 | */ 29 | trait ValueBuilder[T] { 30 | def bson(v: T): BSONValue; 31 | } 32 | 33 | /** 34 | * The '''ValueBuilder''' companion object defines common 35 | * [[reactivemongo.extensions.dsl.criteria.ValueBuilder]] ''type classes'' 36 | * available for any project. Types not known to the library can define 37 | * [[reactivemongo.extensions.dsl.criteria.ValueBuilder]] instances as needed 38 | * to extend the DSL. 39 | */ 40 | object ValueBuilder { 41 | implicit def bsonValueIdentityValue[T <: BSONValue]: ValueBuilder[T] = 42 | new ValueBuilder[T] { 43 | override def bson(v: T): T = v; 44 | } 45 | 46 | implicit object DateTimeValue 47 | extends ValueBuilder[java.util.Date] { 48 | override def bson(v: java.util.Date): BSONValue = 49 | BSONDateTime(v.getTime); 50 | } 51 | 52 | implicit object BooleanValue 53 | extends ValueBuilder[Boolean] { 54 | override def bson(v: Boolean): BSONValue = 55 | BSONBoolean(v); 56 | } 57 | 58 | implicit object DoubleValue 59 | extends ValueBuilder[Double] { 60 | override def bson(v: Double): BSONValue = 61 | BSONDouble(v); 62 | } 63 | 64 | implicit object IntValue 65 | extends ValueBuilder[Int] { 66 | override def bson(v: Int): BSONValue = 67 | BSONInteger(v); 68 | } 69 | 70 | implicit object LongValue 71 | extends ValueBuilder[Long] { 72 | override def bson(v: Long): BSONValue = 73 | BSONLong(v); 74 | } 75 | 76 | implicit object StringValue 77 | extends ValueBuilder[String] { 78 | override def bson(v: String): BSONValue = 79 | BSONString(v); 80 | } 81 | 82 | implicit object SymbolValue 83 | extends ValueBuilder[Symbol] { 84 | override def bson(v: Symbol): BSONValue = 85 | BSONSymbol(v.name); 86 | } 87 | 88 | implicit object TimestampValue 89 | extends ValueBuilder[java.sql.Timestamp] { 90 | override def bson(v: java.sql.Timestamp): BSONValue = 91 | BSONTimestamp(v.getTime); 92 | } 93 | } 94 | 95 | -------------------------------------------------------------------------------- /bson/src/main/scala/fixtures/BsonFixtures.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.fixtures 18 | 19 | import scala.concurrent.{ Future, ExecutionContext } 20 | 21 | import play.api.libs.iteratee.Enumerator 22 | import play.api.libs.json.JsObject 23 | 24 | import reactivemongo.bson.BSONDocument 25 | import reactivemongo.api.DB 26 | import reactivemongo.api.commands.WriteResult 27 | import reactivemongo.api.collections.bson.BSONCollection 28 | import play.modules.reactivemongo.json.BSONFormats 29 | import reactivemongo.extensions.util.Logger 30 | 31 | class BsonFixtures(db: => DB)(implicit ec: ExecutionContext) extends Fixtures[BSONDocument] { 32 | def map(document: JsObject): BSONDocument = 33 | BSONFormats.BSONDocumentFormat.reads(document).get 34 | 35 | def bulkInsert(collectionName: String, documents: Stream[BSONDocument]): Future[Int] = db.collection[BSONCollection]( 36 | collectionName).bulkInsert(documents, ordered = true).map(_.n) 37 | 38 | def removeAll(collectionName: String): Future[WriteResult] = 39 | db.collection[BSONCollection](collectionName). 40 | remove(query = BSONDocument.empty, firstMatchOnly = false) 41 | 42 | def drop(collectionName: String): Future[Unit] = 43 | db.collection[BSONCollection](collectionName).drop() 44 | 45 | } 46 | 47 | object BsonFixtures { 48 | def apply(db: DB)(implicit ec: ExecutionContext): BsonFixtures = 49 | new BsonFixtures(db) 50 | } 51 | 52 | -------------------------------------------------------------------------------- /bson/src/test/resources/bson/events.conf: -------------------------------------------------------------------------------- 1 | # Predefined reusable values 2 | _predef { 3 | location: { 4 | city: Ankara 5 | place: Salon 6 | } 7 | } 8 | 9 | 10 | # "events" collection 11 | events { 12 | 13 | event1 { 14 | _id: _id_event1 15 | title: Developer workshop 16 | organizer: ${persons.person1.fullname} 17 | location: ${_predef.location} 18 | } 19 | 20 | event2 { 21 | _id: _id_event2 22 | title: Some movie 23 | organizer: ${persons.person2.fullname} 24 | location: ${_predef.location} 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /bson/src/test/resources/bson/persons.conf: -------------------------------------------------------------------------------- 1 | # Predefined reusable values 2 | _predef { 3 | country: TC 4 | } 5 | 6 | 7 | # "persons" collection 8 | persons { 9 | 10 | person1 { 11 | _id: _id_person1 12 | name: Ali 13 | surname: Veli 14 | fullname: ${name} ${surname} 15 | age: 32 16 | salary: 999.85 17 | time: 12345678900 18 | country: ${_predef.country} 19 | } 20 | 21 | person2 { 22 | _id: _id_person2 23 | name: Haydar 24 | surname: Cabbar 25 | fullname: ${name} ${surname} 26 | age: ${person1.age} 27 | salary: { "$double": 1000.0 } 28 | time: 12345678999 29 | country: ${_predef.country} 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /bson/src/test/scala/dao/BsonFileDaoSpec.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import java.io.ByteArrayOutputStream 20 | 21 | import org.scalatest._ 22 | import org.scalatest.concurrent._ 23 | import org.scalatest.time.{ Span, Seconds } 24 | import play.api.libs.iteratee.{ Iteratee, Enumerator } 25 | import reactivemongo.api.gridfs.Implicits.DefaultReadFileReader 26 | import reactivemongo.api.gridfs.DefaultFileToSave 27 | import reactivemongo.bson.{ BSONDocument, BSONObjectID } 28 | 29 | import scala.concurrent.ExecutionContext.Implicits.global 30 | 31 | class BsonFileDaoSpec 32 | extends FlatSpec 33 | with Matchers 34 | with ScalaFutures 35 | with BeforeAndAfter 36 | with OneInstancePerTest { 37 | 38 | override implicit def patienceConfig = PatienceConfig(timeout = Span(20, Seconds), interval = Span(1, Seconds)) 39 | 40 | val dao = new BsonFileDao[BSONObjectID](MongoContext.db, "bson-files") {} 41 | 42 | "A BsonFileDao" should "save and remove file" in { 43 | val enumerator = Enumerator.fromStream(getClass.getResourceAsStream("/whyfp90.pdf")) 44 | 45 | val result = for { 46 | save <- dao.save(enumerator, filename = "whyfp90.pdf", contentType = "application/pdf") 47 | id = save.id.asInstanceOf[BSONObjectID] 48 | findBefore <- dao.findById(id) 49 | remove <- dao.removeById(id) 50 | findAfter <- dao.findById(id) 51 | } yield (id, findBefore, findAfter) 52 | 53 | whenReady(result) { 54 | case (id, findBefore, findAfter) => 55 | import org.scalatest.OptionValues._ 56 | findBefore.value.id should be(id) 57 | findBefore.value.length should be(200007) 58 | findAfter should be('empty) 59 | } 60 | } 61 | 62 | it should "find file by name" in { 63 | val enumerator = Enumerator.fromStream(getClass.getResourceAsStream("/whyfp90.pdf")) 64 | 65 | val result = for { 66 | save <- dao.save(enumerator, filename = "whyfp90.pdf", contentType = "application/pdf") 67 | id = save.id.asInstanceOf[BSONObjectID] 68 | find <- dao.findOne(BSONDocument("filename" -> save.filename)) 69 | remove <- dao.removeById(id) 70 | } yield (id, find) 71 | 72 | whenReady(result) { 73 | case (id, find) => 74 | import org.scalatest.OptionValues._ 75 | find.value.id should be(id) 76 | } 77 | } 78 | 79 | it should "enumerate one" in { 80 | val enumerator = Enumerator.fromStream(getClass.getResourceAsStream("/whyfp90.pdf")) 81 | 82 | val length = Iteratee.fold(0) { (state: Int, bytes: Array[Byte]) => 83 | state + bytes.size 84 | } 85 | 86 | val result = for { 87 | save <- dao.save(enumerator, filename = "whyfp90.pdf", contentType = "application/pdf") 88 | id = save.id.asInstanceOf[BSONObjectID] 89 | enumerator <- dao.findOne(BSONDocument("filename" -> save.filename)).enumerate 90 | len <- enumerator.get |>>> length 91 | remove <- dao.removeById(id) 92 | } yield (len) 93 | 94 | whenReady(result) { length => 95 | length shouldBe 200007 96 | } 97 | } 98 | 99 | it should "enumerate by id" in { 100 | val enumerator = Enumerator.fromStream(getClass.getResourceAsStream("/whyfp90.pdf")) 101 | 102 | val length = Iteratee.fold(0) { (state: Int, bytes: Array[Byte]) => 103 | state + bytes.size 104 | } 105 | 106 | val result = for { 107 | save <- dao.save(enumerator, filename = "whyfp90.pdf", contentType = "application/pdf") 108 | id = save.id.asInstanceOf[BSONObjectID] 109 | enumerator <- dao.findById(id).enumerate 110 | len <- enumerator.get |>>> length 111 | remove <- dao.removeById(id) 112 | } yield (len) 113 | 114 | whenReady(result) { length => 115 | length shouldBe 200007 116 | } 117 | } 118 | 119 | it should "read one to outputstream" in { 120 | val enumerator = Enumerator.fromStream(getClass.getResourceAsStream("/whyfp90.pdf")) 121 | val out = new ByteArrayOutputStream 122 | 123 | val result = for { 124 | save <- dao.save(enumerator, filename = "whyfp90.pdf", contentType = "application/pdf") 125 | id = save.id.asInstanceOf[BSONObjectID] 126 | read <- dao.findOne(BSONDocument("filename" -> save.filename)).read(out) 127 | remove <- dao.removeById(id) 128 | } yield read 129 | 130 | whenReady(result) { read => 131 | read shouldBe ('defined) 132 | out.size() shouldBe 200007 133 | } 134 | } 135 | 136 | it should "read by id to outputstream" in { 137 | val enumerator = Enumerator.fromStream(getClass.getResourceAsStream("/whyfp90.pdf")) 138 | val out = new ByteArrayOutputStream 139 | 140 | val result = for { 141 | save <- dao.save(enumerator, filename = "whyfp90.pdf", contentType = "application/pdf") 142 | id = save.id.asInstanceOf[BSONObjectID] 143 | read <- dao.findById(id).read(out) 144 | remove <- dao.removeById(id) 145 | } yield read 146 | 147 | whenReady(result) { read => 148 | read shouldBe ('defined) 149 | out.size() shouldBe 200007 150 | } 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /bson/src/test/scala/dao/CustomIdBsonDao.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import scala.concurrent.ExecutionContext.Implicits.global 20 | import reactivemongo.extensions.model.CustomIdModel 21 | import reactivemongo.api.indexes.{ Index, IndexType } 22 | import reactivemongo.extensions.util.Misc.UUID 23 | 24 | class CustomIdBsonDao extends { 25 | override val autoIndexes = Seq( 26 | Index(Seq("name" -> IndexType.Ascending), unique = true, background = true), 27 | Index(Seq("age" -> IndexType.Ascending), background = true) 28 | ) 29 | } with BsonDao[CustomIdModel, String](MongoContext.db, "customId-" + UUID()) 30 | -------------------------------------------------------------------------------- /bson/src/test/scala/dao/CustomIdBsonDaoSpec.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import org.scalatest._ 20 | import org.scalatest.concurrent._ 21 | import org.scalatest.time.SpanSugar._ 22 | import reactivemongo.bson._ 23 | import reactivemongo.bson.Macros.Options.Verbose 24 | import reactivemongo.extensions.model.CustomIdModel 25 | import reactivemongo.extensions.dsl.BsonDsl._ 26 | import scala.concurrent.Future 27 | import scala.concurrent.ExecutionContext.Implicits.global 28 | 29 | class CustomIdBsonDaoSpec 30 | extends FlatSpec 31 | with Matchers 32 | with ScalaFutures 33 | with BeforeAndAfter 34 | with OneInstancePerTest { 35 | 36 | override implicit def patienceConfig = PatienceConfig(timeout = 20 seconds, interval = 1 seconds) 37 | 38 | val dao = new CustomIdBsonDao 39 | 40 | after { 41 | dao.dropSync() 42 | } 43 | 44 | "A CustomIdBsonDao" should "find document by id" in { 45 | val customIdModel = CustomIdModel(name = "foo", surname = "bar", age = 32) 46 | 47 | val futureResult = for { 48 | insertResult <- dao.insert(customIdModel) 49 | maybeCustomIdModel <- dao.findById(customIdModel._id) 50 | } yield maybeCustomIdModel 51 | 52 | whenReady(futureResult) { maybeCustomIdModel => 53 | maybeCustomIdModel should be('defined) 54 | maybeCustomIdModel.get._id shouldBe customIdModel._id 55 | maybeCustomIdModel.get.age shouldBe customIdModel.age 56 | } 57 | } 58 | 59 | it should "find documents by ids" in { 60 | val customIdModels = CustomIdModel.random(100) 61 | 62 | val futureResult = for { 63 | insertResult <- dao.bulkInsert(customIdModels) 64 | models <- dao.findByIds(customIdModels.drop(5).map(_._id): _*) 65 | } yield models 66 | 67 | whenReady(futureResult) { models => 68 | models should have size 95 69 | } 70 | } 71 | 72 | it should "update document by id" in { 73 | val customIdModel = CustomIdModel(name = "foo", surname = "bar", age = 32) 74 | val update = $set("age" -> 64) 75 | 76 | val futureResult = for { 77 | insert <- dao.insert(customIdModel) 78 | update <- dao.updateById(customIdModel._id, update) 79 | updatedMaybeCustomIdModel <- dao.findById(customIdModel._id) 80 | } yield updatedMaybeCustomIdModel 81 | 82 | whenReady(futureResult) { updatedMaybeCustomIdModel => 83 | updatedMaybeCustomIdModel should be('defined) 84 | val updatedCustomIdModel = updatedMaybeCustomIdModel.get 85 | updatedCustomIdModel._id shouldBe customIdModel._id 86 | updatedCustomIdModel.age shouldBe 64 87 | } 88 | } 89 | 90 | it should "update the whole document by id" in { 91 | val customIdModel = CustomIdModel(name = "foo", surname = "bar", age = 32) 92 | val update = customIdModel.copy(age = 64) 93 | 94 | val futureResult = for { 95 | insert <- dao.insert(customIdModel) 96 | update <- dao.updateById(customIdModel._id, update) 97 | updatedMaybeCustomIdModel <- dao.findById(customIdModel._id) 98 | } yield updatedMaybeCustomIdModel 99 | 100 | whenReady(futureResult) { updatedMaybeCustomIdModel => 101 | updatedMaybeCustomIdModel should be('defined) 102 | val updatedCustomIdModel = updatedMaybeCustomIdModel.get 103 | updatedCustomIdModel._id shouldBe customIdModel._id 104 | updatedCustomIdModel.age shouldBe 64 105 | } 106 | } 107 | 108 | it should "ensure indexes" in { 109 | val futureIndexes = Future { 110 | // Give some time for indexes to be ensured 111 | Thread.sleep(2000) 112 | } flatMap { _ => 113 | dao.listIndexes() 114 | } 115 | 116 | whenReady(futureIndexes) { indexes => 117 | indexes should have size 3 // including _id 118 | } 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /bson/src/test/scala/dao/DummyBsonDao.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import scala.concurrent.ExecutionContext.Implicits.global 20 | import reactivemongo.extensions.model.DummyModel 21 | import reactivemongo.api.indexes.{ Index, IndexType } 22 | import reactivemongo.bson.BSONObjectID 23 | import reactivemongo.bson.DefaultBSONHandlers._ 24 | import reactivemongo.extensions.util.Misc.UUID 25 | 26 | class DummyBsonDao 27 | extends BsonDao[DummyModel, BSONObjectID](MongoContext.db, "dummy-" + UUID()) { 28 | 29 | override def autoIndexes = Seq( 30 | Index(Seq("name" -> IndexType.Ascending), unique = true, background = true), 31 | Index(Seq("age" -> IndexType.Ascending), background = true) 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /bson/src/test/scala/dao/DynamicBsonDaoSpec.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import org.scalatest._ 20 | import org.scalatest.concurrent.{ ScalaFutures } 21 | import org.scalatest.time.{ Span, Seconds } 22 | import reactivemongo.bson.{ BSONObjectID, BSONDocument } 23 | import reactivemongo.extensions.dsl.BsonDsl._ 24 | import reactivemongo.extensions.util.Logger 25 | import reactivemongo.extensions.Implicits._ 26 | import scala.concurrent.{ Future, Await } 27 | import scala.concurrent.ExecutionContext.Implicits.global 28 | 29 | class DynamicBsonDaoSpec 30 | extends FlatSpec 31 | with Matchers 32 | with ScalaFutures 33 | with BeforeAndAfter 34 | with OneInstancePerTest { 35 | 36 | override implicit def patienceConfig = PatienceConfig(timeout = Span(20, Seconds), interval = Span(1, Seconds)) 37 | 38 | val builder = BsonDaoBuilder[BSONDocument, BSONObjectID](MongoContext.db) 39 | 40 | before { 41 | import scala.concurrent.duration._ 42 | Await.ready(builder("collection1").removeAll(), 10 seconds) 43 | Await.ready(builder("collection2").removeAll(), 10 seconds) 44 | } 45 | 46 | "A DynamicBsonDao" should "use different collections" in { 47 | val dao1 = builder("collection1") 48 | val dao2 = builder("collection2") 49 | 50 | val futureResult = for { 51 | insertResult1 <- dao1.insert($doc("name" -> "ali", "surname" -> "veli")) 52 | insertResult2 <- dao2.insert($doc("name" -> "haydar", "surname" -> "cabbar", "age" -> 18)) 53 | result1 <- ~dao1.findOne("name" $eq "ali") 54 | result2 <- ~dao2.findOne("name" $eq "haydar") 55 | insertResult12 <- dao1.insert(result2) 56 | count1 <- dao1.count() 57 | count2 <- dao2.count() 58 | } yield (result1, result2, count1, count2) 59 | 60 | whenReady(futureResult) { 61 | case (result1, result2, count1, count2) => 62 | result1.getAs[String]("surname") should be(Some("veli")) 63 | result2.getAs[String]("surname") should be(Some("cabbar")) 64 | count1 shouldBe 2 65 | count2 shouldBe 1 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /bson/src/test/scala/dao/EventBsonDao.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import scala.concurrent.{ Future, Await } 20 | import scala.concurrent.duration._ 21 | import scala.concurrent.ExecutionContext.Implicits.global 22 | import reactivemongo.extensions.model.Event 23 | import reactivemongo.api.DefaultDB 24 | import reactivemongo.extensions.dsl.BsonDsl 25 | 26 | class EventBsonDao(_db: DefaultDB) 27 | extends BsonDao[Event, String](_db, "events") with BsonDsl { 28 | 29 | def findByTitle(title: String): Future[Option[Event]] = 30 | findOne("title" $eq title) 31 | 32 | def dropDatabaseSync(): Unit = Await.result(_db.drop(), 20 seconds) 33 | } 34 | -------------------------------------------------------------------------------- /bson/src/test/scala/dao/MapModelBsonDao.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import scala.concurrent.ExecutionContext.Implicits.global 20 | import reactivemongo.extensions.model.MapModel 21 | import reactivemongo.bson.BSONObjectID 22 | import reactivemongo.bson.DefaultBSONHandlers._ 23 | import reactivemongo.api.commands.GetLastError 24 | import reactivemongo.extensions.util.Misc.UUID 25 | 26 | class MapModelBsonDao extends BsonDao[MapModel, BSONObjectID]( 27 | MongoContext.db, "dummy-" + UUID()) { 28 | 29 | override def defaultWriteConcern = GetLastError.Default.copy(j = true) 30 | } 31 | -------------------------------------------------------------------------------- /bson/src/test/scala/dao/MapModelBsonDaoSpec.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import org.scalatest._ 20 | import org.scalatest.concurrent._ 21 | import org.scalatest.time.SpanSugar._ 22 | import reactivemongo.bson._ 23 | import reactivemongo.extensions.dsl.BsonDsl._ 24 | import reactivemongo.extensions.model.MapModel 25 | import reactivemongo.extensions.util.Logger 26 | import reactivemongo.extensions.Implicits._ 27 | import scala.concurrent.Future 28 | import scala.concurrent.ExecutionContext.Implicits.global 29 | 30 | class MapModelBsonDaoSpec 31 | extends FlatSpec 32 | with Matchers 33 | with ScalaFutures 34 | with BeforeAndAfter 35 | with OneInstancePerTest { 36 | 37 | override implicit def patienceConfig = PatienceConfig(timeout = 20 seconds, interval = 1 seconds) 38 | 39 | val dao = new MapModelBsonDao 40 | 41 | after { 42 | dao.dropSync() 43 | } 44 | 45 | "A MapModelBsonDao" should "find one document" in { 46 | val mapModel = MapModel(data = Map("count" -> 1)) 47 | 48 | val futureResult = for { 49 | insertResult <- dao.insert(mapModel) 50 | maybeMapModel <- dao.findOne() 51 | } yield maybeMapModel 52 | 53 | whenReady(futureResult) { maybeMapModel => 54 | maybeMapModel should be('defined) 55 | maybeMapModel.get._id shouldBe mapModel._id 56 | maybeMapModel.get.data("count") shouldBe 1 57 | } 58 | } 59 | 60 | it should "support ~ operator" in { 61 | val mapModel = MapModel(data = Map("count" -> 1)) 62 | 63 | val futureResult = for { 64 | insertResult <- dao.insert(mapModel) 65 | mapModel <- ~dao.findOne() 66 | count <- dao.count($id(mapModel._id)) 67 | } yield (mapModel, count) 68 | 69 | whenReady(futureResult) { 70 | case (foundMapModel, count) => 71 | foundMapModel._id shouldBe mapModel._id 72 | foundMapModel.data("count") shouldBe 1 73 | count shouldBe 1 74 | } 75 | } 76 | 77 | it should "throw exception when using ~ operator with None" in { 78 | val mapModel = MapModel(data = Map("count" -> 1)) 79 | 80 | val futureResult = (for { 81 | insertResult <- dao.insert(mapModel) 82 | mapModel <- ~dao.findOne("none" $eq "unknown") 83 | count <- dao.count($id(mapModel._id)) 84 | } yield count) recover { 85 | case ex: java.util.NoSuchElementException => ex 86 | } 87 | 88 | whenReady(futureResult) { ex => 89 | ex shouldBe a[java.util.NoSuchElementException] 90 | } 91 | } 92 | 93 | it should "save document" in { 94 | val mapModel = MapModel(data = Map("count" -> 1)) 95 | 96 | val futureResult = for { 97 | insert <- dao.save(mapModel) 98 | maybeInsertedDummyModel <- dao.findById(mapModel._id) 99 | newData = mapModel.data + ("total" -> 2, "count" -> 2) 100 | update <- dao.save(mapModel.copy(data = newData)) 101 | maybeUpdatedDummyModel <- dao.findById(mapModel._id) 102 | } yield (maybeInsertedDummyModel, maybeUpdatedDummyModel) 103 | 104 | whenReady(futureResult) { 105 | case (maybeInsertedDummyModel, maybeUpdatedDummyModel) => 106 | maybeInsertedDummyModel should be('defined) 107 | val insertedDummyModel = maybeInsertedDummyModel.get 108 | insertedDummyModel._id shouldBe mapModel._id 109 | insertedDummyModel.data("count") shouldBe 1 110 | 111 | maybeUpdatedDummyModel should be('defined) 112 | val updatedDummyModel = maybeUpdatedDummyModel.get 113 | updatedDummyModel._id shouldBe mapModel._id 114 | updatedDummyModel.data("count") shouldBe 2 115 | updatedDummyModel.data("total") shouldBe 2 116 | } 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /bson/src/test/scala/dao/PersonBsonDao.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import scala.concurrent.{ Future, Await } 20 | import scala.concurrent.duration._ 21 | import scala.concurrent.ExecutionContext.Implicits.global 22 | 23 | import reactivemongo.extensions.model.Person 24 | import reactivemongo.api.DefaultDB 25 | import reactivemongo.extensions.dsl.BsonDsl 26 | import reactivemongo.extensions.dao.Handlers._ 27 | 28 | class PersonBsonDao(_db: DefaultDB) 29 | extends BsonDao[Person, String](_db, "persons") with BsonDsl { 30 | 31 | def findByName(name: String): Future[Option[Person]] = 32 | findOne("name" $eq name) 33 | 34 | def dropDatabaseSync(): Unit = Await.result(_db.drop(), 20 seconds) 35 | } 36 | -------------------------------------------------------------------------------- /bson/src/test/scala/dao/TemporalModelDao.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import scala.concurrent.{ Future, Await } 20 | import scala.concurrent.duration._ 21 | import scala.concurrent.ExecutionContext.Implicits.global 22 | import reactivemongo.extensions.model.TemporalModel 23 | import reactivemongo.api.DefaultDB 24 | import reactivemongo.api.indexes.{ Index, IndexType } 25 | import reactivemongo.bson.BSONObjectID 26 | import reactivemongo.extensions.util.Misc.UUID 27 | 28 | class TemporalModelDao 29 | extends BsonDao[TemporalModel, BSONObjectID](MongoContext.db, "temporal_model_" + UUID()) 30 | 31 | -------------------------------------------------------------------------------- /bson/src/test/scala/dao/TemporalModelDaoSpec.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import org.scalatest._ 20 | import org.scalatest.concurrent._ 21 | import org.scalatest.time.SpanSugar._ 22 | import reactivemongo.bson._ 23 | import reactivemongo.bson.Macros.Options.Verbose 24 | import reactivemongo.extensions.model.TemporalModel 25 | import reactivemongo.extensions.dsl.BsonDsl._ 26 | import scala.concurrent.Future 27 | import scala.concurrent.ExecutionContext.Implicits.global 28 | 29 | class TemporalModelDaoSpec 30 | extends FlatSpec 31 | with Matchers 32 | with ScalaFutures 33 | with BeforeAndAfter 34 | with OneInstancePerTest { 35 | 36 | override implicit def patienceConfig = PatienceConfig(timeout = 20 seconds, interval = 1 seconds) 37 | 38 | val dao = new TemporalModelDao 39 | 40 | after { 41 | dao.dropSync() 42 | } 43 | 44 | "A TemporalModelDao" should "update updateAt" in { 45 | val temporalModel = TemporalModel(name = "foo", surname = "bar") 46 | 47 | val futureResult = for { 48 | insertResult <- dao.insert(temporalModel) 49 | maybeTemporalModel <- dao.findById(temporalModel._id) 50 | } yield maybeTemporalModel 51 | 52 | whenReady(futureResult) { maybeTemporalModel => 53 | maybeTemporalModel should be('defined) 54 | maybeTemporalModel.get._id shouldBe temporalModel._id 55 | maybeTemporalModel.get.name shouldBe temporalModel.name 56 | maybeTemporalModel.get.surname shouldBe temporalModel.surname 57 | maybeTemporalModel.get.createdAt shouldBe temporalModel.createdAt 58 | maybeTemporalModel.get.updatedAt.isAfter(temporalModel.updatedAt) shouldBe true 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /bson/src/test/scala/dsl/criteria/UntypedWhereSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Steve Vickers 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * Created on: Jun 15, 2014 17 | */ 18 | package reactivemongo.extensions.dsl.criteria 19 | 20 | import org.scalatest._ 21 | import org.scalatest.matchers._ 22 | 23 | import reactivemongo.bson._ 24 | 25 | /** 26 | * The '''UntypedWhereSpec''' type verifies the behaviour expected of the 27 | * `where` method in the [[reactivemongo.extensions.dsl.criteria.Untyped]] 28 | * `type`. 29 | * 30 | * @author svickers 31 | * 32 | */ 33 | class UntypedWhereSpec 34 | extends FlatSpec 35 | with Matchers { 36 | /// Class Imports 37 | import Untyped._ 38 | 39 | "An Untyped where" should "support 1 placeholder" in 40 | { 41 | val q = where { 42 | _.a === 1 43 | } 44 | 45 | BSONDocument.pretty(q) shouldBe ( 46 | BSONDocument.pretty( 47 | BSONDocument( 48 | "a" -> BSONInteger(1) 49 | ) 50 | ) 51 | ); 52 | } 53 | 54 | it should "support 2 placeholders" in 55 | { 56 | val q = where { 57 | _.a === 1 && _.b === 2 58 | } 59 | 60 | BSONDocument.pretty(q) shouldBe ( 61 | BSONDocument.pretty( 62 | BSONDocument( 63 | "$and" -> 64 | BSONArray( 65 | BSONDocument( 66 | "a" -> BSONInteger(1) 67 | ), 68 | BSONDocument( 69 | "b" -> BSONInteger(2) 70 | ) 71 | ) 72 | ) 73 | ) 74 | ); 75 | } 76 | 77 | it should "support 3 placeholders" in 78 | { 79 | val q = where { 80 | _.a === 1 && _.b === 2 && _.c === 3 81 | } 82 | 83 | BSONDocument.pretty(q) shouldBe ( 84 | BSONDocument.pretty( 85 | BSONDocument( 86 | "$and" -> 87 | BSONArray( 88 | BSONDocument( 89 | "a" -> BSONInteger(1) 90 | ), 91 | BSONDocument( 92 | "b" -> BSONInteger(2) 93 | ), 94 | BSONDocument( 95 | "c" -> BSONInteger(3) 96 | ) 97 | ) 98 | ) 99 | ) 100 | ); 101 | } 102 | 103 | /// The library supports from 1 to 22 placeholders for the where method. 104 | it should "support 22 placeholders" in 105 | { 106 | val q = where { 107 | _.p === 0 && 108 | _.p === 0 && 109 | _.p === 0 && 110 | _.p === 0 && 111 | _.p === 0 && 112 | _.p === 0 && 113 | _.p === 0 && 114 | _.p === 0 && 115 | _.p === 0 && 116 | _.p === 0 && 117 | _.p === 0 && 118 | _.p === 0 && 119 | _.p === 0 && 120 | _.p === 0 && 121 | _.p === 0 && 122 | _.p === 0 && 123 | _.p === 0 && 124 | _.p === 0 && 125 | _.p === 0 && 126 | _.p === 0 && 127 | _.p === 0 && 128 | _.p === 0 129 | } 130 | 131 | BSONDocument.pretty(q) shouldBe ( 132 | BSONDocument.pretty( 133 | BSONDocument( 134 | "$and" -> 135 | BSONArray(List.fill(22)(BSONDocument("p" -> BSONInteger(0)))) 136 | ) 137 | ) 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /bson/src/test/scala/fixtures/BsonFixturesSpec.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.fixtures 18 | 19 | import org.scalatest._ 20 | import org.scalatest.concurrent._ 21 | import org.scalatest.time.{ Span, Seconds } 22 | import reactivemongo.extensions.util.Logger 23 | import reactivemongo.extensions.dao.{ 24 | MongoContext, 25 | PersonBsonDao, 26 | EventBsonDao, 27 | Handlers 28 | }, Handlers._ // extension BSON handler 29 | import reactivemongo.extensions.Implicits.FutureOption // ~ 30 | import scala.concurrent.ExecutionContext.Implicits.global 31 | 32 | class BsonFixturesSpec extends FlatSpec with Matchers with ScalaFutures with BeforeAndAfter { 33 | override implicit def patienceConfig = PatienceConfig(timeout = Span(20, Seconds), interval = Span(1, Seconds)) 34 | 35 | val db = MongoContext.randomDb 36 | val fixtures = BsonFixtures(db) 37 | val personDao = new PersonBsonDao(db) 38 | val eventDao = new EventBsonDao(db) 39 | 40 | after { 41 | db.drop() 42 | } 43 | 44 | "A BsonFixtures" should "load persons" in { 45 | val futureCount = for { 46 | remove <- fixtures.removeAll("bson/persons.conf") 47 | beforeCount <- personDao.count() 48 | remove2 <- fixtures.removeAll("bson/persons.conf") 49 | insert <- fixtures.load("bson/persons.conf") 50 | afterCount <- personDao.count() 51 | person1 <- ~personDao.findByName("Ali") 52 | person2 <- ~personDao.findByName("Haydar") 53 | } yield (beforeCount, afterCount, person1, person2) 54 | 55 | whenReady(futureCount) { 56 | case (beforeCount, afterCount, person1, person2) => 57 | beforeCount shouldBe 0 58 | afterCount shouldBe 2 59 | person1.fullname shouldBe "Ali Veli" 60 | person1.salary shouldBe 999.85 61 | person2.fullname shouldBe "Haydar Cabbar" 62 | person2.salary shouldBe 1000.0 63 | } 64 | } 65 | 66 | it should "load persons and events" in { 67 | val futureCount = for { 68 | remove <- fixtures.removeAll("bson/persons.conf", "bson/events.conf") 69 | beforeCount <- eventDao.count() 70 | insert <- fixtures.load("bson/persons.conf", "bson/events.conf") 71 | afterCount <- eventDao.count() 72 | event2 <- ~eventDao.findByTitle("Some movie") 73 | } yield (beforeCount, afterCount, event2) 74 | 75 | whenReady(futureCount) { 76 | case (beforeCount, afterCount, event2) => 77 | beforeCount shouldBe 0 78 | afterCount shouldBe 2 79 | event2.organizer shouldBe "Haydar Cabbar" 80 | event2.location.city shouldBe "Ankara" 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /bson/src/test/scala/model/CustomIdModel.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.model 18 | 19 | import reactivemongo.bson._ 20 | import reactivemongo.extensions.dao.Handlers._ 21 | 22 | case class CustomIdModel( 23 | _id: String = java.util.UUID.randomUUID.toString, 24 | name: String, 25 | surname: String, 26 | age: Int) 27 | 28 | object CustomIdModel { 29 | implicit val customIdModelHandler = Macros.handler[CustomIdModel] 30 | 31 | def random(n: Int): Seq[CustomIdModel] = 1 to n map { index => 32 | CustomIdModel(name = s"name$index", surname = "surname$index", age = index) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bson/src/test/scala/model/DummyModel.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.model 18 | 19 | import reactivemongo.bson._ 20 | import reactivemongo.extensions.dao.Handlers._ 21 | 22 | case class DummyModel( 23 | _id: BSONObjectID = BSONObjectID.generate, 24 | name: String, 25 | surname: String, 26 | age: Int) 27 | 28 | object DummyModel { 29 | implicit val dummyModelHandler = Macros.handler[DummyModel] 30 | 31 | def random(n: Int): Seq[DummyModel] = 1 to n map { index => 32 | DummyModel(name = s"name$index", surname = "surname$index", age = index) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bson/src/test/scala/model/Event.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.model 18 | 19 | import reactivemongo.bson._ 20 | import reactivemongo.extensions.dao.Handlers._ 21 | import play.api.libs.json.Json 22 | 23 | case class Event( 24 | _id: String, 25 | title: String, 26 | organizer: String, 27 | location: Location) 28 | 29 | case class Location(city: String, place: String) 30 | 31 | object Event { 32 | import reactivemongo.extensions.dao.Handlers._ // extensions BSON handler 33 | 34 | implicit val locationFormat = Macros.handler[Location] 35 | implicit val eventFormat = Macros.handler[Event] 36 | } 37 | -------------------------------------------------------------------------------- /bson/src/test/scala/model/MapModel.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.model 18 | 19 | import reactivemongo.bson._ 20 | import reactivemongo.extensions.dao.Handlers._ 21 | import play.api.libs.json.Json 22 | 23 | case class MapModel( 24 | _id: BSONObjectID = BSONObjectID.generate, 25 | data: Map[String, Int]) 26 | 27 | object MapModel { 28 | implicit val customIdModelHandler = Macros.handler[MapModel] 29 | 30 | def random(n: Int): Seq[MapModel] = 1 to n map { index => 31 | MapModel(data = Map("count" -> n)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /bson/src/test/scala/model/Person.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.model 18 | 19 | import reactivemongo.bson._ 20 | 21 | case class Person( 22 | _id: String, 23 | name: String, 24 | surname: String, 25 | fullname: String, 26 | age: Int, 27 | salary: Double, 28 | time: Long, 29 | country: String) 30 | 31 | object Person { 32 | import reactivemongo.extensions.dao.Handlers._ // extensions BSON handler 33 | implicit val personFormat = Macros.handler[Person] 34 | } 35 | -------------------------------------------------------------------------------- /bson/src/test/scala/model/TemporalModel.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.model 18 | 19 | import reactivemongo.bson._ 20 | import reactivemongo.extensions.dao.LifeCycle 21 | import reactivemongo.extensions.dao.Handlers._ 22 | import reactivemongo.extensions.util.Logger 23 | import org.joda.time.DateTime 24 | 25 | case class TemporalModel( 26 | _id: BSONObjectID = BSONObjectID.generate, 27 | name: String, 28 | surname: String, 29 | createdAt: DateTime = DateTime.now, 30 | updatedAt: DateTime = DateTime.now) 31 | 32 | object TemporalModel { 33 | import reactivemongo.extensions.dao.Handlers._ 34 | 35 | implicit val temporalModelFormat = Macros.handler[TemporalModel] 36 | 37 | implicit object TemporalModelLifeCycle extends LifeCycle[TemporalModel, BSONObjectID] { 38 | def prePersist(model: TemporalModel): TemporalModel = { 39 | Logger.debug(s"prePersist $model") 40 | model.copy(updatedAt = DateTime.now) 41 | } 42 | 43 | def postPersist(model: TemporalModel): Unit = { 44 | Logger.debug(s"postPersist $model") 45 | } 46 | 47 | def preRemove(id: BSONObjectID): Unit = { 48 | Logger.debug(s"preRemove $id") 49 | } 50 | 51 | def postRemove(id: BSONObjectID): Unit = { 52 | Logger.debug(s"postRemove $id") 53 | } 54 | 55 | def ensuredIndexes(): Unit = { 56 | Logger.debug("ensuredIndexes") 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import scalariform.formatter.preferences._ 2 | 3 | name := "reactivemongo-extensions" 4 | 5 | lazy val commonSettings = Seq( 6 | organization := "org.reactivemongo", 7 | version := "0.11.7.play24", 8 | scalaVersion := "2.11.7", 9 | crossScalaVersions := Seq("2.11.7"), 10 | scalacOptions := Seq( 11 | "-unchecked", 12 | "-deprecation", 13 | "-encoding", "utf8", 14 | "-feature", 15 | "-language:higherKinds", 16 | "-language:postfixOps", 17 | "-language:implicitConversions", 18 | "-language:existentials", 19 | "-target:jvm-1.6"), 20 | resolvers ++= Seq( 21 | "Typesafe repository releases" at "http://repo.typesafe.com/typesafe/releases/", 22 | "Sonatype snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/"), 23 | javaOptions in Test ++= Seq("-Xmx512m", "-XX:MaxPermSize=512m"), 24 | testOptions in Test += Tests.Argument("-oDS"), 25 | parallelExecution in Test := true, 26 | shellPrompt in ThisBuild := Common.prompt, 27 | ScalariformKeys.preferences := ScalariformKeys.preferences.value 28 | .setPreference(AlignParameters, true) 29 | .setPreference(DoubleIndentClassDeclaration, true) 30 | .setPreference(MultilineScaladocCommentsStartOnFirstLine, true) 31 | .setPreference(PlaceScaladocAsterisksBeneathSecondAsterisk, true)) 32 | 33 | lazy val publishSettings = Seq( 34 | publishMavenStyle := true, 35 | publishArtifact in Test := false, 36 | publishTo := { 37 | val nexus = "https://oss.sonatype.org/" 38 | if (isSnapshot.value) 39 | Some("snapshots" at nexus + "content/repositories/snapshots") 40 | else 41 | Some("releases" at nexus + "service/local/staging/deploy/maven2") 42 | }, 43 | pomExtra := ( 44 | http://github.com/fehmicansaglam/reactivemongo-extensions 45 | 46 | 47 | Apache 2 48 | http://www.apache.org/licenses/LICENSE-2.0 49 | repo 50 | 51 | 52 | 53 | git@github.com:fehmicansaglam/reactivemongo-extensions.git 54 | scm:git@github.com:fehmicansaglam/reactivemongo-extensions.git 55 | 56 | 57 | 58 | fehmicansaglam 59 | Fehmi Can Saglam 60 | http://github.com/fehmicansaglam 61 | 62 | 63 | osxhacker 64 | Steve Vickers 65 | http://github.com/osxhacker 66 | 67 | )) 68 | 69 | val travisSettings = Seq( 70 | Travis.travisSnapshotBranches := Seq("0.10.x", "0.10.5.akka23-SNAPSHOT"), 71 | commands += Travis.travisCommand 72 | ) 73 | 74 | lazy val settings = ( 75 | commonSettings 76 | ++ travisSettings 77 | ++ scalariformSettings 78 | ++ org.scalastyle.sbt.ScalastylePlugin.Settings) 79 | 80 | lazy val root = project.in(file(".")) 81 | .aggregate(bson, json, core, samples) 82 | .settings(settings: _*) 83 | .settings(publishSettings: _*) 84 | .settings(publishArtifact := false) 85 | .settings(unidocSettings: _*) 86 | 87 | lazy val core = project.in(file("core")) 88 | .settings(settings: _*) 89 | .settings(publishSettings: _*) 90 | 91 | lazy val bson = project.in(file("bson")) 92 | .settings(settings: _*) 93 | .settings(publishSettings: _*) 94 | .dependsOn(core % "test->test;compile->compile") 95 | 96 | lazy val json = project.in(file("json")) 97 | .settings(settings: _*) 98 | .settings(publishSettings: _*) 99 | .dependsOn(core % "test->test;compile->compile") 100 | 101 | lazy val samples = project.in(file("samples")) 102 | .settings(settings: _*) 103 | .settings(publishSettings: _*) 104 | .settings(publishArtifact := false) 105 | .dependsOn(core % "test->test;compile->compile", bson % "compile->compile") 106 | -------------------------------------------------------------------------------- /core/build.sbt: -------------------------------------------------------------------------------- 1 | name := "reactivemongo-extensions-core" 2 | 3 | libraryDependencies ++= Seq( 4 | "org.reactivemongo" %% "reactivemongo" % Common.reactiveMongoVersion, 5 | "com.typesafe.play" %% "play-json" % Common.playVersion % "provided", 6 | "com.typesafe" % "config" % "1.2.1", 7 | "joda-time" % "joda-time" % "2.3", 8 | "org.joda" % "joda-convert" % "1.6", 9 | "org.slf4j" % "slf4j-api" % "1.7.5", 10 | "ch.qos.logback" % "logback-classic" % "1.0.13" % "test", 11 | "org.scalatest" %% "scalatest" % "2.1.5" % "test") 12 | -------------------------------------------------------------------------------- /core/src/main/scala/Implicits.scala: -------------------------------------------------------------------------------- 1 | package reactivemongo.extensions 2 | 3 | import scala.concurrent.{ Future, ExecutionContext } 4 | 5 | object Implicits { 6 | 7 | implicit class FutureOption[T](future: Future[Option[T]])(implicit ec: ExecutionContext) { 8 | 9 | def unary_~ : Future[T] = future.map(_.get) 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /core/src/main/scala/bson.scala: -------------------------------------------------------------------------------- 1 | package reactivemongo.extensions 2 | 3 | import reactivemongo.bson._ 4 | import scala.reflect.runtime.universe.{ TypeTag, typeOf } 5 | 6 | object BsonTypes { 7 | 8 | def numberOf[T <: BSONValue: TypeTag]: Int = typeOf[T] match { 9 | case t if t =:= typeOf[BSONDouble] => 1 10 | case t if t =:= typeOf[BSONString] => 2 11 | case t if t =:= typeOf[BSONDocument] => 3 12 | case t if t =:= typeOf[BSONArray] => 4 13 | case t if t =:= typeOf[BSONBinary] => 5 14 | case t if t =:= typeOf[BSONUndefined.type] => 6 15 | case t if t =:= typeOf[BSONObjectID] => 7 16 | case t if t =:= typeOf[BSONBoolean] => 8 17 | case t if t =:= typeOf[BSONDateTime] => 9 18 | case t if t =:= typeOf[BSONNull.type] => 10 19 | case t if t =:= typeOf[BSONRegex] => 11 20 | case t if t =:= typeOf[BSONDBPointer] => 12 21 | case t if t =:= typeOf[BSONJavaScript] => 13 22 | case t if t =:= typeOf[BSONSymbol] => 14 23 | case t if t =:= typeOf[BSONJavaScriptWS] => 15 24 | case t if t =:= typeOf[BSONInteger] => 16 25 | case t if t =:= typeOf[BSONTimestamp] => 17 26 | case t if t =:= typeOf[BSONLong] => 18 27 | case t if t =:= typeOf[BSONMinKey.type] => 255 28 | case t if t =:= typeOf[BSONMaxKey.type] => 127 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /core/src/main/scala/dao/Dao.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import scala.concurrent.{ Future, ExecutionContext } 20 | import scala.concurrent.duration.Duration 21 | 22 | import reactivemongo.api.{ DB, Collection, CollectionProducer } 23 | import reactivemongo.api.indexes.Index 24 | import reactivemongo.api.commands.{ GetLastError, WriteResult } 25 | 26 | /** 27 | * Base class for all DAO implementations. This class defines the API for all DAOs. 28 | * 29 | * A DAO defines how to work with a specific collection. 30 | * 31 | * @param db A parameterless function returning a [[reactivemongo.api.DB]] instance. 32 | * @param collectionName Name of the collection this DAO is going to operate on. 33 | * @tparam C Type of the collection. 34 | * @tparam Structure The type that C operates on. `BSONDocument` or `JsObject`. 35 | * @tparam Model Type of the model that this DAO uses. 36 | * @tparam ID Type of the ID field of the model. 37 | * @tparam Writer the `Structure` writer 38 | */ 39 | abstract class Dao[C <: Collection: CollectionProducer, Structure, Model, ID, Writer[_]](db: => DB, collectionName: String) { 40 | 41 | /** 42 | * The list of indexes to be ensured on DAO load. 43 | * 44 | * Because of Scala initialization order there are exactly 2 ways 45 | * of defining auto indexes. 46 | * 47 | * First way is to use an '''early definition''': 48 | * 49 | * {{{ 50 | * object PersonDao extends { 51 | * override val autoIndexes = Seq( 52 | * Index(Seq("name" -> IndexType.Ascending), unique = true, background = true), 53 | * Index(Seq("age" -> IndexType.Ascending), background = true)) 54 | * } with BsonDao[Person, BSONObjectID](MongoContext.db, "persons") 55 | * }}} 56 | * 57 | * Second way is to '''override def'''. Be careful __not to change declaration to `val` instead of `def`__. 58 | * 59 | * {{{ 60 | * object PersonDao extends BsonDao[Person, BSONObjectID](MongoContext.db, "persons") { 61 | * 62 | * override def autoIndexes = Seq( 63 | * Index(Seq("name" -> IndexType.Ascending), unique = true, background = true), 64 | * Index(Seq("age" -> IndexType.Ascending), background = true)) 65 | * } 66 | * }}} 67 | */ 68 | def autoIndexes: Traversable[Index] = Seq.empty 69 | 70 | /** 71 | * Bulk inserts multiple models. 72 | * 73 | * @param models A [[scala.collection.TraversableOnce]] of models. 74 | * @param bulkSize 75 | * @param bulkByteSize 76 | * @return The number of successful insertions. 77 | */ 78 | def bulkInsert(models: TraversableOnce[Model], bulkSize: Int, bulkByteSize: Int)(implicit ec: ExecutionContext): Future[Int] 79 | 80 | /** Reference to the collection this DAO operates on. */ 81 | def collection: C = db.collection[C](collectionName) 82 | 83 | /** 84 | * Returns the number of documents in this collection matching the given selector. 85 | * 86 | * @param selector Selector document which may be empty. 87 | */ 88 | def count(selector: Structure)(implicit ec: ExecutionContext): Future[Int] 89 | 90 | /** 91 | * Defines the default write concern for this Dao which defaults to `GetLastError()`. 92 | * 93 | * Related API functions should allow overriding this value. 94 | */ 95 | def defaultWriteConcern: GetLastError = GetLastError.Default 96 | 97 | /** Drops this collection */ 98 | def drop()(implicit ec: ExecutionContext): Future[Unit] 99 | 100 | /** 101 | * Drops this collection and awaits until it has been dropped or a timeout has occured. 102 | * @param timeout Maximum amount of time to await until this collection has been dropped. 103 | * @return true if the collection has been successfully dropped, otherwise false. 104 | */ 105 | def dropSync(timeout: Duration)(implicit ec: ExecutionContext): Unit 106 | 107 | /** Ensures indexes defined by `autoIndexes`. */ 108 | def ensureIndexes()(implicit ec: ExecutionContext): Future[Traversable[Boolean]] 109 | 110 | /** 111 | * Retrieves models by page matching the given selector. 112 | * 113 | * @param selector Selector document. 114 | * @param sort Sorting document. 115 | * @param page 1 based page number. 116 | * @param pageSize Maximum number of elements in each page. 117 | */ 118 | def find(selector: Structure, sort: Structure, page: Int, pageSize: Int)(implicit ec: ExecutionContext): Future[List[Model]] 119 | 120 | /** 121 | * Retrieves all models matching the given selector. 122 | * 123 | * @param selector Selector document. 124 | * @param sort Sorting document. 125 | */ 126 | def findAll(selector: Structure, sort: Structure)(implicit ec: ExecutionContext): Future[List[Model]] 127 | 128 | /** 129 | * Updates and returns a single model. It returns the old document by default. 130 | * 131 | * @param query The selection criteria for the update. 132 | * @param update Performs an update of the selected model. 133 | * @param sort Determines which model the operation updates if the query selects multiple models. 134 | * findAndUpdate() updates the first model in the sort order specified by this argument. 135 | * @param fetchNewObject When true, returns the updated model rather than the original. 136 | * @param upsert When true, findAndUpdate() creates a new model if no model matches the query. 137 | */ 138 | def findAndUpdate( 139 | query: Structure, 140 | update: Structure, 141 | sort: Structure, 142 | fetchNewObject: Boolean, 143 | upsert: Boolean)(implicit ec: ExecutionContext): Future[Option[Model]] 144 | 145 | /** 146 | * Removes and returns a single model. 147 | * 148 | * @param query The selection criteria for the remove. 149 | * @param sort Determines which model the operation removes if the query selects multiple models. 150 | * findAndRemove() removes the first model in the sort order specified by this argument. 151 | */ 152 | def findAndRemove(query: Structure, sort: Structure)(implicit ec: ExecutionContext): Future[Option[Model]] 153 | 154 | /** Retrieves the model with the given `id`. */ 155 | def findById(id: ID)(implicit ec: ExecutionContext): Future[Option[Model]] 156 | 157 | /** Retrieves the models with the given `ids`. */ 158 | def findByIds(ids: ID*)(implicit ec: ExecutionContext): Future[List[Model]] 159 | 160 | /** Retrieves at most one model matching the given selector. */ 161 | def findOne(selector: Structure)(implicit ec: ExecutionContext): Future[Option[Model]] 162 | 163 | /** 164 | * Retrieves a random model matching the given selector. 165 | * 166 | * This API may require more than one query. 167 | */ 168 | def findRandom(selector: Structure)(implicit ec: ExecutionContext): Future[Option[Model]] 169 | 170 | /** 171 | * Folds the documents matching the given selector by applying the function `f`. 172 | * 173 | * @param selector Selector document. 174 | * @param sort Sorting document. 175 | * @param state Initial state for the fold operation. 176 | * @param f Folding function. 177 | * @tparam A Type of fold result. 178 | */ 179 | def fold[A](selector: Structure, sort: Structure, state: A)(f: (A, Model) => A)(implicit ec: ExecutionContext): Future[A] 180 | 181 | /** 182 | * Iterates over the documents matching the given selector and applies the function `f`. 183 | * 184 | * @param selector Selector document. 185 | * @param sort Sorting document. 186 | * @param f function to be applied. 187 | */ 188 | def foreach(selector: Structure, sort: Structure)(f: (Model) => Unit)(implicit ec: ExecutionContext): Future[Unit] 189 | 190 | /** Inserts the given model. */ 191 | def insert(model: Model, writeConcern: GetLastError)(implicit ec: ExecutionContext): Future[WriteResult] 192 | 193 | /** 194 | * Lists indexes that are currently ensured in this collection. 195 | * 196 | * This list may not be equal to `autoIndexes` in case of index creation failure. 197 | */ 198 | def listIndexes()(implicit ec: ExecutionContext): Future[List[Index]] 199 | 200 | /** 201 | * Removes model(s) matching the given selector. 202 | * 203 | * In order to remove multiple documents `firstMatchOnly` has to be `false`. 204 | * 205 | * @param selector Selector document. 206 | * @param writeConcern Write concern defaults to `defaultWriteConcern`. 207 | * @param firstMatchOnly Remove only the first matching document. 208 | */ 209 | def remove( 210 | selector: Structure, 211 | writeConcern: GetLastError, 212 | firstMatchOnly: Boolean)(implicit ec: ExecutionContext): Future[WriteResult] 213 | 214 | /** Removes all documents in this collection. */ 215 | def removeAll(writeConcern: GetLastError)(implicit ec: ExecutionContext): Future[WriteResult] 216 | 217 | /** Removes the document with the given ID. */ 218 | def removeById(id: ID, writeConcern: GetLastError)(implicit ec: ExecutionContext): Future[WriteResult] 219 | 220 | /** 221 | * Inserts the document, or updates it if it already exists in the collection. 222 | * 223 | * @param model The model to save. 224 | * @param writeConcern the [[reactivemongo.core.commands.GetLastError]] command message to send in order to control 225 | * how the document is inserted. Defaults to defaultWriteConcern. 226 | */ 227 | def save(model: Model, writeConcern: GetLastError)(implicit ec: ExecutionContext): Future[WriteResult] 228 | 229 | /** 230 | * Updates the documents matching the given selector. 231 | * 232 | * @param selector Selector query. 233 | * @param update Update query. 234 | * @param writeConcern Write concern which defaults to defaultWriteConcern. 235 | * @param upsert Create the document if it does not exist. 236 | * @param multi Update multiple documents. 237 | * @tparam U Type of the update query. 238 | */ 239 | def update[U: Writer]( 240 | selector: Structure, 241 | update: U, 242 | writeConcern: GetLastError, 243 | upsert: Boolean, 244 | multi: Boolean)(implicit ec: ExecutionContext): Future[WriteResult] 245 | 246 | /** 247 | * Updates the document with the given `id`. 248 | * 249 | * @param id ID of the document that will be updated. 250 | * @param update Update query. 251 | * @param writeConcern Write concern which defaults to defaultWriteConcern. 252 | * @tparam U Type of the update query. 253 | */ 254 | def updateById[U: Writer](id: ID, update: U, writeConcern: GetLastError)(implicit ec: ExecutionContext): Future[WriteResult] 255 | } 256 | -------------------------------------------------------------------------------- /core/src/main/scala/dao/FileDao.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import java.io.OutputStream 20 | 21 | import scala.concurrent.{ Future, ExecutionContext } 22 | 23 | import play.api.libs.iteratee.Enumerator 24 | 25 | import reactivemongo.api.{ BSONSerializationPack, DBMetaCommands, Cursor, DB } 26 | import reactivemongo.api.gridfs.{ 27 | DefaultFileToSave, 28 | ReadFile, 29 | FileToSave, 30 | GridFS, 31 | IdProducer, 32 | Implicits 33 | }, Implicits.DefaultReadFileReader 34 | import reactivemongo.bson.{ BSONDocumentReader, BSONDocumentWriter, BSONValue } 35 | import reactivemongo.api.commands.WriteResult 36 | 37 | import reactivemongo.extensions.dao.FileDao.ReadFileWrapper 38 | 39 | /** 40 | * Base class for all File DAO implementations. 41 | * 42 | * @param db A [[reactivemongo.api.DB]] instance. 43 | * @param collectionName Name of the collection this DAO is going to operate on. 44 | */ 45 | abstract class FileDao[Id <% BSONValue: IdProducer, Structure]( 46 | db: => DB with DBMetaCommands, collectionName: String) { 47 | 48 | import FileDao.BSONReadFile 49 | 50 | /** Reference to the GridFS instance this FileDao operates on. */ 51 | lazy val gfs = GridFS(db, collectionName) 52 | 53 | /** 54 | * Finds the files matching the given selector. 55 | * 56 | * @param selector Selector document 57 | * @return A cursor for the files matching the given selector. 58 | */ 59 | def find(selector: Structure)(implicit sWriter: BSONDocumentWriter[Structure], ec: ExecutionContext): Cursor[BSONReadFile] = gfs.find[Structure, BSONReadFile](selector) 60 | 61 | /** Retrieves the file with the given `id`. */ 62 | def findById(id: Id)(implicit ec: ExecutionContext): ReadFileWrapper 63 | 64 | /* Retrieves at most one file matching the given selector. */ 65 | def findOne(selector: Structure)(implicit sWriter: BSONDocumentWriter[Structure], ec: ExecutionContext): ReadFileWrapper = 66 | ReadFileWrapper(gfs, gfs.find(selector).headOption) 67 | 68 | /** Removes the file with the given `id`. */ 69 | def removeById(id: Id)(implicit ec: ExecutionContext): Future[WriteResult] = 70 | gfs.remove(implicitly[BSONValue](id)) 71 | 72 | /** Saves the content provided by the given enumerator with the given metadata. */ 73 | def save( 74 | enumerator: Enumerator[Array[Byte]], 75 | file: FileToSave[BSONSerializationPack.type, BSONValue], 76 | chunkSize: Int = 262144)(implicit readFileReader: BSONDocumentReader[BSONReadFile], ec: ExecutionContext): Future[BSONReadFile] = 77 | gfs.save(enumerator, file, chunkSize) 78 | 79 | /** Saves the content provided by the given enumerator with the given metadata. */ 80 | def save( 81 | enumerator: Enumerator[Array[Byte]], 82 | filename: String, 83 | contentType: String)(implicit readFileReader: BSONDocumentReader[BSONReadFile], ec: ExecutionContext): Future[BSONReadFile] = 84 | gfs.save(enumerator, DefaultFileToSave( 85 | filename = filename, contentType = Some(contentType))) 86 | 87 | } 88 | 89 | object FileDao { 90 | type BSONReadFile = ReadFile[BSONSerializationPack.type, BSONValue] 91 | 92 | case class ReadFileWrapper(gfs: GridFS[BSONSerializationPack.type], readFile: Future[Option[BSONReadFile]]) { 93 | 94 | def enumerate(implicit ec: ExecutionContext): Future[Option[Enumerator[Array[Byte]]]] = readFile.map(_.map(gfs.enumerate(_))) 95 | 96 | def read(out: OutputStream)(implicit ec: ExecutionContext): Future[Option[Unit]] = readFile.flatMap { 97 | case Some(readFile) => gfs.readToOutputStream(readFile, out).map(Some(_)) 98 | case None => Future.successful(None) 99 | } 100 | } 101 | 102 | implicit def readFileWrapperToReadFile(readFileWrapper: ReadFileWrapper): Future[Option[BSONReadFile]] = readFileWrapper.readFile 103 | 104 | } 105 | -------------------------------------------------------------------------------- /core/src/main/scala/dao/Handlers.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import org.joda.time.DateTime 20 | import reactivemongo.bson._ 21 | 22 | object Handlers { 23 | 24 | implicit object BSONDateTimeHandler 25 | extends BSONReader[BSONDateTime, DateTime] 26 | with BSONWriter[DateTime, BSONDateTime] { 27 | 28 | def read(bson: BSONDateTime): DateTime = new DateTime(bson.value) 29 | 30 | def write(date: DateTime) = BSONDateTime(date.getMillis) 31 | } 32 | 33 | implicit def MapBSONReader[T](implicit reader: BSONReader[_ <: BSONValue, T]): BSONDocumentReader[Map[String, T]] = 34 | new BSONDocumentReader[Map[String, T]] { 35 | def read(doc: BSONDocument): Map[String, T] = { 36 | doc.elements.collect { 37 | case (key, value) => value.seeAsOpt[T](reader) map { 38 | ov => (key, ov) 39 | } 40 | }.flatten.toMap 41 | } 42 | } 43 | 44 | implicit def MapBSONWriter[T](implicit writer: BSONWriter[T, _ <: BSONValue]): BSONDocumentWriter[Map[String, T]] = new BSONDocumentWriter[Map[String, T]] { 45 | def write(doc: Map[String, T]): BSONDocument = { 46 | BSONDocument(doc.toTraversable map (t => (t._1, writer.write(t._2)))) 47 | } 48 | } 49 | 50 | implicit def MapReader[V](implicit vr: BSONDocumentReader[V]): BSONDocumentReader[Map[String, V]] = new BSONDocumentReader[Map[String, V]] { 51 | def read(bson: BSONDocument): Map[String, V] = { 52 | val elements = bson.elements.map { tuple => 53 | // assume that all values in the document are BSONDocuments 54 | tuple._1 -> vr.read(tuple._2.seeAsTry[BSONDocument].get) 55 | } 56 | elements.toMap 57 | } 58 | } 59 | 60 | implicit def MapWriter[V](implicit vw: BSONDocumentWriter[V]): BSONDocumentWriter[Map[String, V]] = new BSONDocumentWriter[Map[String, V]] { 61 | def write(map: Map[String, V]): BSONDocument = { 62 | val elements = map.toStream.map { tuple => 63 | tuple._1 -> vw.write(tuple._2) 64 | } 65 | BSONDocument(elements) 66 | } 67 | } 68 | 69 | implicit object BSONIntegerHandler extends BSONReader[BSONValue, Int] { 70 | def read(bson: BSONValue) = bson.asOpt[BSONNumberLike] match { 71 | case Some(num) => num.toInt 72 | case _ => bson match { 73 | case doc @ BSONDocument(_) => 74 | doc.getAs[BSONNumberLike]("$int").map(_.toInt).get 75 | } 76 | } 77 | } 78 | 79 | implicit object BSONLongHandler extends BSONReader[BSONValue, Long] { 80 | def read(bson: BSONValue) = bson.asOpt[BSONNumberLike] match { 81 | case Some(num) => num.toLong 82 | case _ => bson match { 83 | case doc @ BSONDocument(_) => 84 | doc.getAs[BSONNumberLike]("$long").map(_.toLong).get 85 | } 86 | } 87 | } 88 | 89 | implicit object BSONDoubleHandler extends BSONReader[BSONValue, Double] { 90 | def read(bson: BSONValue) = bson.asOpt[BSONNumberLike] match { 91 | case Some(num) => num.toDouble 92 | case _ => bson match { 93 | case doc @ BSONDocument(_) => 94 | doc.getAs[BSONNumberLike]("$double").map(_.toDouble).get 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /core/src/main/scala/dao/LifeCycle.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | /** 20 | * Type class to work with the life cycle of a model. 21 | * 22 | * By defining a life cycle object, one can preprocess all models before being persisted or perform specific actions 23 | * after life cycle events. 24 | * 25 | * This can be useful for updating temporal fields on all model instances before persisting. 26 | * 27 | * {{{ 28 | * import reactivemongo.bson._ 29 | * import reactivemongo.extensions.dao.LifeCycle 30 | * import reactivemongo.extensions.dao.Handlers._ 31 | * import reactivemongo.extensions.util.Logger 32 | * import org.joda.time.DateTime 33 | * 34 | * case class TemporalModel( 35 | * _id: BSONObjectID = BSONObjectID.generate, 36 | * name: String, 37 | * surname: String, 38 | * createdAt: DateTime = DateTime.now, 39 | * updatedAt: DateTime = DateTime.now) 40 | * 41 | * object TemporalModel { 42 | * implicit val temporalModelFormat = Macros.handler[TemporalModel] 43 | * 44 | * implicit object TemporalModelLifeCycle extends LifeCycle[TemporalModel, BSONObjectID] { 45 | * 46 | * def prePersist(model: TemporalModel): TemporalModel = { 47 | * Logger.debug(s"prePersist $model") 48 | * model.copy(updatedAt = DateTime.now) 49 | * } 50 | * 51 | * def postPersist(model: TemporalModel): Unit = { 52 | * Logger.debug(s"postPersist $model") 53 | * } 54 | * 55 | * def preRemove(id: BSONObjectID): Unit = { 56 | * Logger.debug(s"preRemove $id") 57 | * } 58 | * 59 | * def postRemove(id: BSONObjectID): Unit = { 60 | * Logger.debug(s"postRemove $id") 61 | * } 62 | * 63 | * def ensuredIndexes(): Unit = { 64 | * Logger.debug("ensuredIndexes") 65 | * } 66 | * } 67 | * } 68 | * }}} 69 | */ 70 | trait LifeCycle[Model, ID] { 71 | def prePersist(model: Model): Model 72 | def postPersist(model: Model): Unit 73 | def preRemove(id: ID): Unit 74 | def postRemove(id: ID): Unit 75 | def ensuredIndexes(): Unit 76 | } 77 | 78 | /** 79 | * This is the default life cycle for all models. 80 | * 81 | * Basically it does not perform any actions after life cyle events nor does any transformations to model instances. 82 | */ 83 | class ReflexiveLifeCycle[Model, ID] extends LifeCycle[Model, ID] { 84 | def prePersist(model: Model): Model = model 85 | def postPersist(model: Model): Unit = {} 86 | def preRemove(id: ID): Unit = {} 87 | def postRemove(id: ID): Unit = {} 88 | def ensuredIndexes(): Unit = {} 89 | } 90 | -------------------------------------------------------------------------------- /core/src/main/scala/fixtures/Fixtures.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.fixtures 18 | 19 | import com.typesafe.config.{ Config, ConfigFactory, ConfigRenderOptions, ConfigResolveOptions } 20 | import scala.collection.JavaConversions._ 21 | import scala.concurrent.ExecutionContext.Implicits.global 22 | import scala.concurrent.Future 23 | import reactivemongo.extensions.util.Logger 24 | import reactivemongo.api.commands.WriteResult 25 | import play.api.libs.iteratee.Enumerator 26 | import play.api.libs.json.{ Json, JsObject } 27 | 28 | trait Fixtures[T] { 29 | 30 | protected lazy val renderOptions = ConfigRenderOptions.concise.setJson(true).setFormatted(true) 31 | protected lazy val resolveOptions = ConfigResolveOptions.defaults.setAllowUnresolved(true) 32 | protected lazy val reserved = Set("_predef") 33 | 34 | def map(document: JsObject): T 35 | def bulkInsert(collectionName: String, documents: Stream[T]): Future[Int] 36 | def removeAll(collectionName: String): Future[WriteResult] 37 | def drop(collectionName: String): Future[Unit] 38 | 39 | protected def toString(config: Config): String = 40 | config.root.render(renderOptions) 41 | 42 | protected def toJson(config: Config): JsObject = 43 | Json.parse(toString(config)).as[JsObject] 44 | 45 | protected def resolveConfig(config: Config): Config = { 46 | val resolvedConfig = (config.root.keySet diff reserved).foldLeft(config) { (config, collectionName) => 47 | val collectionConfig = config.getConfig(collectionName) 48 | 49 | val resolvedCollectionConfig = collectionConfig.root.keySet.foldLeft(collectionConfig) { (_collectionConfig, documentName) => 50 | val documentConfig = _collectionConfig.getConfig(documentName).resolve(resolveOptions) 51 | _collectionConfig.withValue(documentName, documentConfig.root) 52 | }.resolve(resolveOptions) 53 | 54 | config.withValue(collectionName, resolvedCollectionConfig.root) 55 | }.resolve 56 | 57 | Logger.debug("Resolved Config =>\n" + toString(resolvedConfig)) 58 | resolvedConfig 59 | } 60 | 61 | protected def processCollection(collectionName: String, collectionConfig: Config): Future[Int] = { 62 | val documents = collectionConfig.root.keySet map { documentName => 63 | val documentConfig = collectionConfig.getConfig(documentName) 64 | val document = toJson(documentConfig) 65 | Logger.debug(s"Processing ${documentName}: ${document}") 66 | map(document) 67 | } 68 | 69 | bulkInsert(collectionName, documents.toStream) 70 | } 71 | 72 | protected def foreachCollection[A](resource: String, resources: String*)(f: (Config, String) => Future[A]): Future[Seq[A]] = { 73 | val config = resources.foldLeft(ConfigFactory.parseResources(this.getClass.getClassLoader, resource)) { (config, resource) => 74 | config.withFallback(ConfigFactory.parseResources(this.getClass.getClassLoader, resource)) 75 | } 76 | 77 | val resolvedConfig = resolveConfig(config) 78 | 79 | Future.sequence { (resolvedConfig.root.keySet diff reserved).toSeq map (f(resolvedConfig, _)) } 80 | } 81 | 82 | def load(resource: String, resources: String*): Future[Seq[Int]] = 83 | foreachCollection(resource, resources: _*) { (config, collectionName) => 84 | Logger.debug(s"Processing ${collectionName}.") 85 | processCollection(collectionName, config.getConfig(collectionName)) 86 | } 87 | 88 | def removeAll(resource: String, resources: String*): Future[Seq[WriteResult]] = foreachCollection(resource, resources: _*) { (_, collectionName) => 89 | Logger.debug(s"Removing all documents from ${collectionName}.") 90 | removeAll(collectionName) 91 | } 92 | 93 | def dropAll(resource: String, resources: String*): Future[Seq[Unit]] = 94 | foreachCollection(resource, resources: _*) { (_, collectionName) => 95 | Logger.debug(s"Removing all documents from ${collectionName}.") 96 | drop(collectionName) 97 | } 98 | } 99 | 100 | -------------------------------------------------------------------------------- /core/src/main/scala/util/Logger.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.util 18 | 19 | import org.slf4j.{ LoggerFactory, Logger => Slf4jLogger } 20 | import scala.util.control.NonFatal 21 | 22 | /** 23 | * Typical logger interface. 24 | */ 25 | trait LoggerLike { 26 | 27 | /** 28 | * The underlying SLF4J Logger. 29 | */ 30 | def logger: Slf4jLogger 31 | 32 | /** 33 | * `true` if the logger instance is enabled for the `TRACE` level. 34 | */ 35 | def isTraceEnabled = logger.isTraceEnabled 36 | 37 | /** 38 | * `true` if the logger instance is enabled for the `DEBUG` level. 39 | */ 40 | def isDebugEnabled = logger.isDebugEnabled 41 | 42 | /** 43 | * `true` if the logger instance is enabled for the `INFO` level. 44 | */ 45 | def isInfoEnabled = logger.isInfoEnabled 46 | 47 | /** 48 | * `true` if the logger instance is enabled for the `WARN` level. 49 | */ 50 | def isWarnEnabled = logger.isWarnEnabled 51 | 52 | /** 53 | * `true` if the logger instance is enabled for the `ERROR` level. 54 | */ 55 | def isErrorEnabled = logger.isErrorEnabled 56 | 57 | /** 58 | * Logs a message with the `TRACE` level. 59 | * 60 | * @param message the message to log 61 | */ 62 | def trace(message: => String) { 63 | if (logger.isTraceEnabled) logger.trace(message) 64 | } 65 | 66 | /** 67 | * Logs a message with the `TRACE` level. 68 | * 69 | * @param message the message to log 70 | * @param error the associated exception 71 | */ 72 | def trace(message: => String, error: => Throwable) { 73 | if (logger.isTraceEnabled) logger.trace(message, error) 74 | } 75 | 76 | /** 77 | * Logs a message with the `DEBUG` level. 78 | * 79 | * @param message the message to log 80 | */ 81 | def debug(message: => String) { 82 | if (logger.isDebugEnabled) logger.debug(message) 83 | } 84 | 85 | /** 86 | * Logs a message with the `DEBUG` level. 87 | * 88 | * @param message the message to log 89 | * @param error the associated exception 90 | */ 91 | def debug(message: => String, error: => Throwable) { 92 | if (logger.isDebugEnabled) logger.debug(message, error) 93 | } 94 | 95 | /** 96 | * Logs a message with the `INFO` level. 97 | * 98 | * @param message the message to log 99 | */ 100 | def info(message: => String) { 101 | if (logger.isInfoEnabled) logger.info(message) 102 | } 103 | 104 | /** 105 | * Logs a message with the `INFO` level. 106 | * 107 | * @param message the message to log 108 | * @param error the associated exception 109 | */ 110 | def info(message: => String, error: => Throwable) { 111 | if (logger.isInfoEnabled) logger.info(message, error) 112 | } 113 | 114 | /** 115 | * Logs a message with the `WARN` level. 116 | * 117 | * @param message the message to log 118 | */ 119 | def warn(message: => String) { 120 | if (logger.isWarnEnabled) logger.warn(message) 121 | } 122 | 123 | /** 124 | * Logs a message with the `WARN` level. 125 | * 126 | * @param message the message to log 127 | * @param error the associated exception 128 | */ 129 | def warn(message: => String, error: => Throwable) { 130 | if (logger.isWarnEnabled) logger.warn(message, error) 131 | } 132 | 133 | /** 134 | * Logs a message with the `ERROR` level. 135 | * 136 | * @param message the message to log 137 | */ 138 | def error(message: => String) { 139 | if (logger.isErrorEnabled) logger.error(message) 140 | } 141 | 142 | /** 143 | * Logs a message with the `ERROR` level. 144 | * 145 | * @param message the message to log 146 | * @param error the associated exception 147 | */ 148 | def error(message: => String, error: => Throwable) { 149 | if (logger.isErrorEnabled) logger.error(message, error) 150 | } 151 | 152 | } 153 | 154 | /** 155 | * A Play logger. 156 | * 157 | * @param logger the underlying SL4FJ logger 158 | */ 159 | class Logger(val logger: Slf4jLogger) extends LoggerLike 160 | 161 | /** 162 | * High-level API for logging operations. 163 | * 164 | * For example, logging with the default application logger: 165 | * {{{ 166 | * Logger.info("Hello!") 167 | * }}} 168 | * 169 | * Logging with a custom logger: 170 | * {{{ 171 | * Logger("my.logger").info("Hello!") 172 | * }}} 173 | * 174 | */ 175 | object Logger extends LoggerLike { 176 | 177 | /** 178 | * The 'application' logger. 179 | */ 180 | lazy val logger = LoggerFactory.getLogger("reactivemongo.extensions") 181 | 182 | /** 183 | * Obtains a logger instance. 184 | * 185 | * @param name the name of the logger 186 | * @return a logger 187 | */ 188 | def apply(name: String): Logger = new Logger(LoggerFactory.getLogger(name)) 189 | 190 | /** 191 | * Obtains a logger instance. 192 | * 193 | * @param clazz a class whose name will be used as logger name 194 | * @return a logger 195 | */ 196 | def apply[T](clazz: Class[T]): Logger = new Logger(LoggerFactory.getLogger(clazz)) 197 | 198 | } 199 | 200 | -------------------------------------------------------------------------------- /core/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %coloredLevel %logger{15} - %message%n%xException{5}%caller{0} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /core/src/test/resources/whyfp90.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveMongo/ReactiveMongo-Extensions/f7b98ac168c534051583079da7dfd9a6750eca0d/core/src/test/resources/whyfp90.pdf -------------------------------------------------------------------------------- /core/src/test/scala/dao/MongoContext.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import reactivemongo.api.{ MongoDriver, DefaultDB } 20 | import reactivemongo.extensions.util.Misc.UUID 21 | import scala.concurrent.ExecutionContext.Implicits.global 22 | 23 | object MongoContext { 24 | val driver = new MongoDriver 25 | val connection = driver.connection(List("localhost")) 26 | def db: DefaultDB = connection("test-reactivemongo-extensions") 27 | def randomDb: DefaultDB = connection(UUID()) 28 | } 29 | -------------------------------------------------------------------------------- /core/src/test/scala/util/ColoredLevel.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.util 18 | 19 | import ch.qos.logback.classic.Level 20 | import ch.qos.logback.classic.spi.ILoggingEvent 21 | import ch.qos.logback.classic.pattern.ClassicConverter 22 | 23 | /** 24 | * A logback converter generating colored, lower-case level names. 25 | * 26 | * Used for example as: 27 | * {{{ 28 | * %coloredLevel %logger{15} - %message%n%xException{5} 29 | * }}} 30 | */ 31 | class ColoredLevel extends ClassicConverter { 32 | 33 | def convert(event: ILoggingEvent): String = { 34 | event.getLevel match { 35 | case Level.TRACE => "[" + Colors.blue("trace") + "]" 36 | case Level.DEBUG => "[" + Colors.cyan("debug") + "]" 37 | case Level.INFO => "[" + Colors.white("info") + "]" 38 | case Level.WARN => "[" + Colors.yellow("warn") + "]" 39 | case Level.ERROR => "[" + Colors.red("error") + "]" 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /core/src/test/scala/util/Colors.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009-2013 Typesafe Inc. 3 | */ 4 | package reactivemongo.extensions.util 5 | 6 | object Colors { 7 | 8 | import scala.Console._ 9 | 10 | lazy val isANSISupported = { 11 | Option(System.getProperty("sbt.log.noformat")).map(_ != "true").orElse { 12 | Option(System.getProperty("os.name")) 13 | .map(_.toLowerCase) 14 | .filter(_.contains("windows")) 15 | .map(_ => false) 16 | }.getOrElse(true) 17 | } 18 | 19 | def red(str: String): String = if (isANSISupported) (RED + str + RESET) else str 20 | def blue(str: String): String = if (isANSISupported) (BLUE + str + RESET) else str 21 | def cyan(str: String): String = if (isANSISupported) (CYAN + str + RESET) else str 22 | def green(str: String): String = if (isANSISupported) (GREEN + str + RESET) else str 23 | def magenta(str: String): String = if (isANSISupported) (MAGENTA + str + RESET) else str 24 | def white(str: String): String = if (isANSISupported) (WHITE + str + RESET) else str 25 | def black(str: String): String = if (isANSISupported) (BLACK + str + RESET) else str 26 | def yellow(str: String): String = if (isANSISupported) (YELLOW + str + RESET) else str 27 | 28 | } 29 | -------------------------------------------------------------------------------- /core/src/test/scala/util/Misc.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.util 18 | 19 | object Misc { 20 | 21 | def UUID(): String = java.util.UUID.randomUUID.toString 22 | 23 | } 24 | -------------------------------------------------------------------------------- /guide/bsondao.md: -------------------------------------------------------------------------------- 1 | ## BsonDao 2 | 3 | ### Usage 4 | 5 | BsonDao operates on reactivemongo.api.collections.default.BSONCollection. You will need to define a DAO for each of your models(case classes). 6 | 7 | Below is a sample model. 8 | 9 | ```scala 10 | import reactivemongo.bson._ 11 | import reactivemongo.extensions.dao.Handlers._ 12 | 13 | case class Person( 14 | _id: BSONObjectID = BSONObjectID.generate, 15 | name: String, 16 | surname: String, 17 | age: Int) 18 | 19 | object Person { 20 | implicit val personHandler = Macros.handler[Person] 21 | } 22 | ``` 23 | 24 | To define a BsonDao for the Person model you just need to extend BsonDao. 25 | 26 | ```scala 27 | import reactivemongo.api.{ MongoDriver, DB } 28 | import reactivemongo.bson.BSONObjectID 29 | import reactivemongo.bson.DefaultBSONHandlers._ 30 | import reactivemongo.extensions.dao.BsonDao 31 | import scala.concurrent.ExecutionContext.Implicits.global 32 | 33 | object MongoContext { 34 | val driver = new MongoDriver 35 | val connection = driver.connection(List("localhost")) 36 | def db: DB = connection("reactivemongo-extensions") 37 | } 38 | 39 | object PersonDao extends BsonDao[Person, BSONObjectID](MongoContext.db, "persons") 40 | ``` 41 | 42 | As seen in the example above ```db``` and ```collectionName``` are the only required parameters of BsonDao. 43 | If you want your indexes to be ensured on DAO load, you can modify the DAO definition like below. 44 | 45 | ```scala 46 | import reactivemongo.api.indexes.{ Index, IndexType } 47 | 48 | object PersonDao extends { 49 | override val autoIndexes = Seq( 50 | Index(Seq("name" -> IndexType.Ascending), unique = true, background = true), 51 | Index(Seq("age" -> IndexType.Ascending), background = true) 52 | ) 53 | } with BsonDao[Person, BSONObjectID](MongoContext.db, "persons") { 54 | // some high level db functions 55 | } 56 | ``` 57 | 58 | ### API 59 | 60 | * **bulkInsert** Bulk inserts multiple models. `prePersist` life cycle event is called for each element *before* this function and `postPersist` is called for each element after this function. 61 | 62 | ```scala 63 | def bulkInsert( 64 | documents: TraversableOnce[Model], 65 | bulkSize: Int = bulk.MaxDocs, 66 | bulkByteSize: Int = bulk.MaxBulkSize): Future[Int] 67 | ``` 68 | 69 | * **count** Returns the number of documents in this collection matching the given selector. 70 | 71 | ```scala 72 | def count(selector: BSONDocument = BSONDocument.empty): Future[Int] 73 | ``` 74 | 75 | * **drop** Drops this collection 76 | 77 | ```scala 78 | def drop(): Future[Boolean] 79 | ``` 80 | 81 | * **dropSync** Drops this collection and awaits until it has been dropped or a timeout has occured. 82 | 83 | ```scala 84 | def dropSync(timeout: Duration): Boolean 85 | ``` 86 | 87 | * **find** Retrieves models by page matching the given selector. 88 | 89 | ```scala 90 | def find( 91 | selector: BSONDocument = BSONDocument.empty, 92 | sort: BSONDocument = BSONDocument("_id" -> 1), 93 | page: Int, 94 | pageSize: Int): Future[List[Model]] 95 | ``` 96 | 97 | * **findAll** Retrieves all models matching the given selector. 98 | 99 | ```scala 100 | def findAll( 101 | selector: BSONDocument = BSONDocument.empty, 102 | sort: BSONDocument = BSONDocument("_id" -> 1)): Future[List[Model]] 103 | ``` 104 | 105 | * **findAndUpdate** Updates and returns a single model. It returns the old document by default. 106 | 107 | ```scala 108 | def findAndUpdate( 109 | query: BSONDocument, 110 | update: BSONDocument, 111 | sort: BSONDocument = BSONDocument.empty, 112 | fetchNewObject: Boolean = false, 113 | upsert: Boolean = false): Future[Option[Model]] 114 | ``` 115 | 116 | * **findAndRemove** Removes and returns a single model. 117 | 118 | ```scala 119 | def findAndRemove( 120 | query: BSONDocument, 121 | sort: BSONDocument = BSONDocument.empty): Future[Option[Model]] 122 | ``` 123 | 124 | * **findById** Retrieves the model with the given `id`. 125 | 126 | ```scala 127 | def findById(id: ID): Future[Option[Model]] 128 | ``` 129 | 130 | * **findByIds** Retrieves the models with the given `ids`. 131 | 132 | ```scala 133 | def findByIds(ids: ID*): Future[List[Model]] 134 | ``` 135 | 136 | * **findOne** Retrieves at most one model matching the given selector. 137 | 138 | ```scala 139 | def findOne(selector: BSONDocument = BSONDocument.empty): Future[Option[Model]] 140 | ``` 141 | 142 | * **findRandom** Retrieves a random model matching the given selector. 143 | 144 | ```scala 145 | def findRandom(selector: BSONDocument = BSONDocument.empty): Future[Option[Model]] 146 | ``` 147 | 148 | * **fold** Folds the documents matching the given selector by applying the function `f`. 149 | 150 | ```scala 151 | def fold[A]( 152 | selector: BSONDocument = BSONDocument.empty, 153 | sort: BSONDocument = BSONDocument("_id" -> 1), 154 | state: A)(f: (A, Model) => A): Future[A] 155 | ``` 156 | 157 | * **foreach** Iterates over the documents matching the given selector and applies the function `f`. 158 | 159 | ```scala 160 | def foreach( 161 | selector: BSONDocument = BSONDocument.empty, 162 | sort: BSONDocument = BSONDocument("_id" -> 1))(f: (Model) => Unit): Future[Unit] 163 | ``` 164 | 165 | * **insert** Inserts the given model. `prePersist` life cycle event is called *before* this function and `postPersist` is called after this function. 166 | 167 | 168 | ```scala 169 | def insert(model: Model, writeConcern: GetLastError = defaultWriteConcern): Future[LastError] 170 | ``` 171 | 172 | * **listIndexes** Lists indexes that are currently ensured in this collection. 173 | 174 | ```scala 175 | def listIndexes(): Future[List[Index]] 176 | ``` 177 | 178 | * **remove** Removes model(s) matching the given selector. 179 | 180 | ```scala 181 | def remove( 182 | query: BSONDocument, 183 | writeConcern: GetLastError = defaultWriteConcern, 184 | firstMatchOnly: Boolean = false): Future[LastError] 185 | ``` 186 | 187 | * **removeAll** Removes all documents in this collection. 188 | 189 | ```scala 190 | def removeAll(writeConcern: GetLastError = defaultWriteConcern): Future[LastError] 191 | ``` 192 | 193 | * **removeById** Removes the document with the given ID. `preRemove` life cycle event is called *before* this function and `postRemove` is called after this function. 194 | 195 | ```scala 196 | def removeById(id: ID, writeConcern: GetLastError = defaultWriteConcern): Future[LastError] 197 | ``` 198 | 199 | * **save** Inserts the document, or updates it if it already exists in the collection. `prePersist` life cycle event is called *before* this function and `postPersist` is called after this function. 200 | 201 | ```scala 202 | def save(model: Model, writeConcern: GetLastError = defaultWriteConcern): Future[LastError] 203 | ``` 204 | 205 | * **update** Updates the documents matching the given selector. 206 | 207 | ```scala 208 | def update[U: BSONDocumentWriter]( 209 | selector: BSONDocument, 210 | update: U, 211 | writeConcern: GetLastError = defaultWriteConcern, 212 | upsert: Boolean = false, 213 | multi: Boolean = false): Future[LastError] 214 | ``` 215 | 216 | * **updateById** Updates the document with the given `id`. 217 | 218 | ```scala 219 | def updateById[U: BSONDocumentWriter]( 220 | id: ID, 221 | update: U, 222 | writeConcern: GetLastError = defaultWriteConcern): Future[LastError] 223 | ``` 224 | -------------------------------------------------------------------------------- /guide/criteria.md: -------------------------------------------------------------------------------- 1 | Criteria 2 | ====================== 3 | 4 | Adds a Criteria DSL for creating [ReactiveMongo](https://github.com/ReactiveMongo/ReactiveMongo) queries 5 | 6 | 7 | ## Overview 8 | 9 | ### Original Query Syntax 10 | 11 | The `reactivemongo.api.collections.GenericCollection` type provides the `find` method used to find documents matching a criteria. It is this interaction which the DSL targets. Originally, providing a selector to `find` had an interaction similar to: 12 | 13 | ```scala 14 | val cursor = collection.find(BSONDocument("firstName" -> "Jack")).cursor[BSONDocument] 15 | ``` 16 | 17 | This is, of course, still supported as the DSL does not preclude this usage. 18 | 19 | ### Criteria DSL 20 | 21 | What the DSL *does* provide is the ablity to formulate queries thusly: 22 | 23 | ```scala 24 | // Using an Untyped.criteria 25 | { 26 | import Untyped._ 27 | 28 | // The MongoDB properties referenced are not enforced by the compiler 29 | // to belong to any particular type. This is what is meant by "Untyped". 30 | val adhoc = criteria.firstName === "Jack" && criteria.age >= 18; 31 | val cursor = collection.find(adhoc).cursor[BSONDocument]; 32 | } 33 | ``` 34 | 35 | Another form which achieves the same result is to use one of the `where` methods available: 36 | 37 | ```scala 38 | // Using one of the Untyped.where overloads 39 | { 40 | import Untyped._ 41 | 42 | val cursor = collection.find( 43 | where (_.firstName === "Jack" && _.age >= 18) 44 | ).cursor[BSONDocument]; 45 | } 46 | ``` 47 | 48 | There are overloads for between 1 and 22 place holders using the `where` method. Should more than 22 be needed, then the 1 argument version should be used with a named parameter. This allows an infinite number of property constraints to be specified. 49 | 50 | For situations where the MongoDB document structure is well known and a developer wishes enforce property existence, the `Typed` Criteria can be used: 51 | 52 | ```scala 53 | { 54 | // Using a Typed criteria which restricts properties to the 55 | // given type. 56 | import Typed._ 57 | 58 | case class ExampleDocument (aProperty : String, another : Int) 59 | 60 | val byKnownProperties = criteria[ExampleDocument].aProperty =~ "^[A-Z]\\w+" && 61 | criteria[ExampleDocument].another > 0; 62 | val cursor = collection.find(byKnownProperties).cursor[BSONDocument]; 63 | } 64 | ``` 65 | 66 | Note that `Typed` and `Untyped` serve different needs. When the structure of a document collection is both known ''and'' identified as static, `Typed` makes sense to employ. However, `Untyped` is compelling when document structure can vary within a collection. These are considerations which can easily vary between projects and even within different modules of one project. 67 | 68 | ### Roadmap 69 | 70 | This section details the functionality either currently or planned to be supported by ReactiveMongo-Criteria. 71 | 72 | - Ability to formulate queries without requiring knowledge of document structure. *COMPLETE* 73 | - Ability to ''type check'' query constraints by specifying a Scala type. *IN PROGRESS* 74 | - Define and add support for an [EDSL](http://scalamacros.org/usecases/advanced-domain-specific-languages.html) specific to [projections](https://github.com/ReactiveMongo/ReactiveMongo/blob/master/driver/src/test/scala/CommonUseCases.scala). *TBD* 75 | 76 | 77 | ## Operators 78 | 79 | When using the Criteria DSL, the fact that the operators adhere to the expectations of both programmers and Scala precedences, most uses will "just work." For example, explicitly defining grouping is done with parentheses, just as you would do with any other bit of Scala code. 80 | 81 | For the purposes of the operator API reference, assume the following code is in scope: 82 | 83 | ```scala 84 | import reactivemongo.extensions.dsl.criteria.Untyped._ 85 | ``` 86 | 87 | ### Comparison Operators 88 | 89 | With the majority of comparison operators, keep in mind that the definition of their ordering is dependent on the type involved. For example, strings will use lexigraphical ordering whereas numbers use natural ordering. 90 | 91 | * **===**, **@==** Matches properties based on value equality. 92 | 93 | ```scala 94 | criteria.aProperty === "value" 95 | ``` 96 | 97 | ```scala 98 | criteria.aProperty @== "value" 99 | ``` 100 | 101 | * **<>**, **=/=**, **!==** Matches properties which do not have the given value. 102 | 103 | ```scala 104 | criteria.aProperty <> "value" 105 | ``` 106 | 107 | ```scala 108 | criteria.aProperty =/= "value" 109 | ``` 110 | 111 | ```scala 112 | criteria.aProperty !== "value" 113 | ``` 114 | 115 | * **<** Matches properties which compare "less than" a given value. 116 | 117 | ```scala 118 | criteria.aNumber < 99 119 | ``` 120 | 121 | * **<=** Matches properties which compare "less than or equal to" a given value. 122 | 123 | ```scala 124 | criteria.aNumber <= 99 125 | ``` 126 | 127 | * **>** Matches properties which compare "greater than" a given value. 128 | 129 | ```scala 130 | criteria.aProperty > "Alice" 131 | ``` 132 | 133 | * **>=** Matches properties which compare "greater than or equal to" a given value. 134 | 135 | ```scala 136 | criteria.aNumber >= 100 137 | ``` 138 | 139 | ### Existence Operators 140 | 141 | * **exists** Matches any document which has the specified field. 142 | 143 | ```scala 144 | criteria.aProperty.exists 145 | ``` 146 | 147 | * **in** Matches properties which are arrays and have one of the given values. 148 | 149 | ```scala 150 | criteria.anArray.in (1, 2, 3, 4, 5) 151 | ``` 152 | 153 | * **all** Matches array properties which contain all of the given values. 154 | 155 | ```scala 156 | criteria.strings.all ("hello", "world") 157 | ``` 158 | 159 | ### String Operators 160 | 161 | * **=~** Matches a string property which satisfies the given regular expression `String`. 162 | 163 | ```scala 164 | criteria.aProperty =~ """^(value)|(someting\s+else)""" 165 | ``` 166 | 167 | * **!~** Matches a string property which does _not_ satisfy the given regular expression `String`. 168 | 169 | ```scala 170 | criteria.aProperty !~ """\d+""" 171 | ``` 172 | 173 | ### Logical Operators 174 | 175 | * **!** The unary not operator provides logical negation of an `Expression`. 176 | 177 | ```scala 178 | !(criteria.aProperty === "value") 179 | ``` 180 | 181 | * **&&** Defines logical conjunction (''AND''). 182 | 183 | ```scala 184 | criteria.aProperty === "value" && criteria.another > 0 185 | ``` 186 | 187 | * **!&&** Defines negation of conjunction (''NOR''). 188 | 189 | ```scala 190 | criteria.aProperty === "value" !&& criteria.aProperty @== "other value" 191 | ``` 192 | 193 | * **||** Defines logical disjunction (''OR''). 194 | 195 | ```scala 196 | criteria.aProperty === "value" || criteria.aProperty === "other value" 197 | ``` 198 | 199 | -------------------------------------------------------------------------------- /guide/dsl.md: -------------------------------------------------------------------------------- 1 | # Query DSL 2 | 3 | ## Query Selectors 4 | 5 | ### Comparison Operators 6 | 7 | * **$eq** Matches values that are equal to the value specified in the query. 8 | 9 | ```scala 10 | "name" $eq "foo" 11 | ``` 12 | 13 | * **$gt** Matches values that are greater than the value specified in the query. 14 | * **$gte** Matches values that are greater than or equal to the value specified in the query. 15 | 16 | ```scala 17 | "age" $gt 18 18 | "age" $gte 18 19 | ``` 20 | 21 | * **$in** Matches any of the values that exist in an array specified in the query. 22 | 23 | ```scala 24 | "age" $in (1, 2, 3) 25 | ``` 26 | 27 | * **$lt** Matches values that are less than the value specified in the query. 28 | * **$lte** Matches values that are less than or equal to the value specified in the query. 29 | 30 | ```scala 31 | "age" $lt 18 32 | "age" $lte 18 33 | ``` 34 | 35 | * **$ne** Matches all values that are not equal to the value specified in the query. 36 | 37 | ```scala 38 | "name" $ne "foo" 39 | ``` 40 | 41 | * **$nin** Matches values that do not exist in an array specified to the query. 42 | 43 | ```scala 44 | "age" $nin (1, 2, 3) 45 | ``` 46 | 47 | ### Logical Operators 48 | 49 | * **$or** Joins query clauses with a logical OR returns all documents that match the conditions of either clause. 50 | 51 | ```scala 52 | $or("qty" $lt 20 $gte 10, "sale" $eq true) 53 | ``` 54 | 55 | * **$and** Joins query clauses with a logical AND returns all documents that match the conditions of both clauses. 56 | 57 | ```scala 58 | $and("name" $eq "foo", "surname" $eq "bar", "age" $eq 32) 59 | ``` 60 | 61 | * **$not** Inverts the effect of a query expression and returns documents that do not match the query expression. 62 | 63 | ```scala 64 | "price" $not { _ $gte 5.1 } 65 | ``` 66 | 67 | * **$nor** Joins query clauses with a logical NOR returns all documents that fail to match both clauses. 68 | 69 | ```scala 70 | $nor("price" $eq 1.99, "qty" $lt 20, "sale" $eq true) 71 | ``` 72 | 73 | ### Element Operators 74 | 75 | * **$exists** Matches documents that have the specified field. 76 | 77 | ```scala 78 | "qty" $exists true 79 | "qty" $exists false 80 | ``` 81 | 82 | * **$type** Selects documents if a field is of the specified type. 83 | 84 | ```scala 85 | "qty".$type[BSONDouble] 86 | "qty".$type[BSONNull.type] 87 | ``` 88 | 89 | ### Evaluation Operators 90 | 91 | * **$mod** Performs a modulo operation on the value of a field and selects documents with a specified result. 92 | 93 | ```scala 94 | "qty" $mod (5, 0) 95 | ``` 96 | 97 | * **$regex** Selects documents where values match a specified regular expression. 98 | 99 | ```scala 100 | "name" $regex ("^Al.*", "i") 101 | ``` 102 | 103 | * **$text** Performs text search. 104 | 105 | ```scala 106 | $text("bake coffee cake") 107 | $text("bake coffee cake", "turkish") 108 | ``` 109 | 110 | * **$where** Matches documents that satisfy a JavaScript expression. 111 | 112 | ```scala 113 | $where("function () { this.credits == this.debits }") 114 | ``` 115 | 116 | ### Array Operators 117 | 118 | * **$all** Matches arrays that contain all elements specified in the query. 119 | 120 | ```scala 121 | "size" $all ("S", "M", "L") 122 | ``` 123 | 124 | * **$elemMatch** Selects documents if element in the array field matches all the specified **$elemMatch** condition. 125 | 126 | ```scala 127 | "array" $elemMatch ("value1" $eq 1, "value2" $gt 1) 128 | ``` 129 | 130 | * **$size** Selects documents if the array field is a specified size. 131 | 132 | ```scala 133 | "comments" $size 12 134 | ``` 135 | 136 | ## Update Operators 137 | 138 | ### Field Update Operators 139 | 140 | * **$inc** Increments the value of the field by the specified amount. 141 | 142 | ```scala 143 | $inc("sold" -> 1, "stock" -> -1) 144 | ``` 145 | 146 | * **$mul** Multiplies the value of the field by the specified amount. 147 | 148 | ```scala 149 | $mul("price" -> 1.25) 150 | ``` 151 | 152 | * **$rename** Renames a field. 153 | 154 | ```scala 155 | $rename("color" -> "colour", "realize" -> "realise") 156 | ``` 157 | 158 | * **$setOnInsert** Sets the value of a field upon document creation during an upsert. Has no effect on update operations that modify existing documents. 159 | 160 | ```scala 161 | $setOnInsert("defaultQty" -> 500, "inStock" -> true) ++ $set("item" -> "apple") 162 | ``` 163 | 164 | * **$set** Sets the value of a field in a document. 165 | 166 | ```scala 167 | $set("name" -> "foo", "surname" -> "bar", "age" -> 32) 168 | ``` 169 | 170 | * **$unset** Removes the specified field from a document. 171 | 172 | ```scala 173 | $unset("name", "surname", "age") 174 | ``` 175 | 176 | * **$min** Only updates the field if the specified value is less than the existing field value. 177 | 178 | ```scala 179 | $min("lowScore" -> 150) 180 | ``` 181 | 182 | * **$max** Only updates the field if the specified value is greater than the existing field value. 183 | 184 | ```scala 185 | $max("highScore" -> 950) 186 | ``` 187 | 188 | * **$currentDate** Sets the value of a field to current date, either as a Date or a Timestamp. 189 | 190 | ```scala 191 | $currentDate("lastModified" -> true, "lastModifiedTS" -> "timestamp") 192 | ``` 193 | 194 | ### Array Update Operators 195 | 196 | * **$addToSet** Adds elements to an array only if they do not already exist in the set. 197 | 198 | ```scala 199 | $addToSet("sizes" -> "L", "colours" -> "Blue") 200 | ``` 201 | 202 | * **$pop** Removes the first or last item of an array. 203 | 204 | ```scala 205 | $pop("scores" -> -1) 206 | $pop("scores" -> 1) 207 | ``` 208 | 209 | * **$pull** Removes all array elements that match a specified query. 210 | 211 | ```scala 212 | $pull("flags", "msr") 213 | $pull("votes" $gte 6) 214 | ``` 215 | 216 | * **$push** Adds an item to an array. 217 | 218 | ```scala 219 | $push("scores", 89) 220 | ``` 221 | -------------------------------------------------------------------------------- /guide/jsondao.md: -------------------------------------------------------------------------------- 1 | ## JsonDao 2 | 3 | ### Usage 4 | 5 | JsonDao operates on play.modules.reactivemongo.json.collection.JSONCollection. 6 | If you are using Play-ReactiveMongo plugin you will need to use JsonDao. 7 | 8 | Below is a sample model. 9 | 10 | ```scala 11 | import reactivemongo.bson.BSONObjectID 12 | 13 | case class Person( 14 | _id: BSONObjectID = BSONObjectID.generate, 15 | name: String, 16 | surname: String, 17 | age: Int) 18 | ``` 19 | 20 | Now let's define a companion object and a JsonDao for this model. 21 | As a best practice companion object should not contain DB related functions. 22 | These functions should be in a DAO object. 23 | Companion object should provide helper functions for the model like transformations, validation, etc. 24 | 25 | ```scala 26 | import play.modules.reactivemongo.ReactiveMongoPlugin 27 | import play.modules.reactivemongo.json.BSONFormats._ 28 | 29 | object Person { 30 | implicit val personFormat = Json.format[Person] 31 | } 32 | 33 | object PersonDao 34 | extends JsonDao[Person, BSONObjectID](ReactiveMongoPlugin.db, "persons"){ 35 | // some high level db functions 36 | } 37 | ``` 38 | 39 | As seen in the example above ```db``` and ```collectionName``` are the only required parameters of JsonDao. 40 | If you want your indexes to be ensured on DAO load, you can modify the DAO definition like below. 41 | 42 | ```scala 43 | import reactivemongo.api.indexes.{ Index, IndexType } 44 | 45 | object PersonDao extends { 46 | override val autoIndexes = Seq( 47 | Index(Seq("name" -> IndexType.Ascending), unique = true, background = true), 48 | Index(Seq("age" -> IndexType.Ascending), background = true) 49 | ) 50 | } with JsonDao[Person, BSONObjectID](() => ReactiveMongoPlugin.db, "persons") { 51 | // some high level db functions 52 | } 53 | ``` 54 | 55 | ### API 56 | 57 | * **bulkInsert** Bulk inserts multiple models. `prePersist` life cycle event is called for each element *before* this function and `postPersist` is called for each element after this function. 58 | 59 | ```scala 60 | def bulkInsert( 61 | documents: TraversableOnce[Model], 62 | bulkSize: Int = bulk.MaxDocs, 63 | bulkByteSize: Int = bulk.MaxBulkSize): Future[Int] 64 | ``` 65 | 66 | * **count** Returns the number of documents in this collection matching the given selector. 67 | 68 | ```scala 69 | def count(selector: JsObject = Json.obj()): Future[Int] 70 | ``` 71 | 72 | * **drop** Drops this collection 73 | 74 | ```scala 75 | def drop(): Future[Boolean] 76 | ``` 77 | 78 | * **dropSync** Drops this collection and awaits until it has been dropped or a timeout has occured. 79 | 80 | ```scala 81 | def dropSync(timeout: Duration): Boolean 82 | ``` 83 | 84 | * **find** Retrieves models by page matching the given selector. 85 | 86 | ```scala 87 | def find( 88 | selector: JsObject = Json.obj(), 89 | sort: JsObject = Json.obj("_id" -> 1), 90 | page: Int, 91 | pageSize: Int): Future[List[Model]] 92 | ``` 93 | 94 | * **findAll** Retrieves all models matching the given selector. 95 | 96 | ```scala 97 | def findAll( 98 | selector: JsObject = Json.obj(), 99 | sort: JsObject = Json.obj("_id" -> 1)): Future[List[Model]] 100 | ``` 101 | 102 | * **findAndUpdate** Updates and returns a single model. It returns the old document by default. 103 | 104 | ```scala 105 | def findAndUpdate( 106 | query: JsObject, 107 | update: JsObject, 108 | sort: JsObject = Json.obj(), 109 | fetchNewObject: Boolean = false, 110 | upsert: Boolean = false): Future[Option[Model]] 111 | ``` 112 | 113 | * **findAndRemove** Removes and returns a single model. 114 | 115 | ```scala 116 | def findAndRemove( 117 | query: JsObject, 118 | sort: JsObject = Json.obj()): Future[Option[Model]] 119 | ``` 120 | 121 | * **findById** Retrieves the model with the given `id`. 122 | 123 | ```scala 124 | def findById(id: ID): Future[Option[Model]] 125 | ``` 126 | 127 | * **findByIds** Retrieves the models with the given `ids`. 128 | 129 | ```scala 130 | def findByIds(ids: ID*): Future[List[Model]] 131 | ``` 132 | 133 | * **findOne** Retrieves at most one model matching the given selector. 134 | 135 | ```scala 136 | def findOne(selector: JsObject = Json.obj()): Future[Option[Model]] 137 | ``` 138 | 139 | * **findRandom** Retrieves a random model matching the given selector. 140 | 141 | ```scala 142 | def findRandom(selector: JsObject = Json.obj()): Future[Option[Model]] 143 | ``` 144 | 145 | * **fold** Folds the documents matching the given selector by applying the function `f`. 146 | 147 | ```scala 148 | def fold[A]( 149 | selector: JsObject = Json.obj(), 150 | sort: JsObject = Json.obj("_id" -> 1), 151 | state: A)(f: (A, Model) => A): Future[A] 152 | ``` 153 | 154 | * **foreach** Iterates over the documents matching the given selector and applies the function `f`. 155 | 156 | ```scala 157 | def foreach( 158 | selector: JsObject = Json.obj(), 159 | sort: JsObject = Json.obj("_id" -> 1))(f: (Model) => Unit): Future[Unit] 160 | ``` 161 | 162 | * **insert** Inserts the given model. `prePersist` life cycle event is called *before* this function and `postPersist` is called after this function. 163 | 164 | ```scala 165 | def insert(model: Model, writeConcern: GetLastError = defaultWriteConcern): Future[LastError] 166 | ``` 167 | 168 | * **listIndexes** Lists indexes that are currently ensured in this collection. 169 | 170 | ```scala 171 | def listIndexes(): Future[List[Index]] 172 | ``` 173 | 174 | * **remove** Removes model(s) matching the given selector. 175 | 176 | ```scala 177 | def remove( 178 | query: JsObject, 179 | writeConcern: GetLastError = defaultWriteConcern, 180 | firstMatchOnly: Boolean = false): Future[LastError] 181 | ``` 182 | 183 | * **removeAll** Removes all documents in this collection. 184 | 185 | ```scala 186 | def removeAll(writeConcern: GetLastError = defaultWriteConcern): Future[LastError] 187 | ``` 188 | 189 | * **removeById** Removes the document with the given ID. `preRemove` life cycle event is called *before* this function and `postRemove` is called after this function. 190 | 191 | 192 | ```scala 193 | def removeById(id: ID, writeConcern: GetLastError = defaultWriteConcern): Future[LastError] 194 | ``` 195 | 196 | * **save** Inserts the document, or updates it if it already exists in the collection. `prePersist` life cycle event is called *before* this function and `postPersist` is called after this function. 197 | 198 | 199 | ```scala 200 | def save(model: Model, writeConcern: GetLastError = GetLastError()): Future[LastError] 201 | ``` 202 | 203 | * **update** Updates the documents matching the given selector. 204 | 205 | ```scala 206 | def update[U: Writes]( 207 | selector: JsObject, 208 | update: U, 209 | writeConcern: GetLastError = defaultWriteConcern, 210 | upsert: Boolean = false, 211 | multi: Boolean = false): Future[LastError] 212 | ``` 213 | 214 | * **updateById** Updates the document with the given `id`. 215 | 216 | ```scala 217 | def updateById[U: Writes]( 218 | id: ID, 219 | update: U, 220 | writeConcern: GetLastError = defaultWriteConcern): Future[LastError] 221 | ``` 222 | -------------------------------------------------------------------------------- /json/build.sbt: -------------------------------------------------------------------------------- 1 | import Common.{ playVersion, playReactiveMongoVersion } 2 | 3 | name := "reactivemongo-extensions-json" 4 | 5 | libraryDependencies ++= Seq( 6 | "org.reactivemongo" %% "play2-reactivemongo" % playReactiveMongoVersion, 7 | "com.typesafe.play" %% "play-json" % playVersion % "provided" 8 | ) 9 | -------------------------------------------------------------------------------- /json/src/main/scala/dao/JsonDao.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.json.dao 18 | 19 | import scala.util.Random 20 | 21 | import scala.concurrent.{ Future, Await, ExecutionContext } 22 | 23 | import scala.concurrent.duration._ 24 | import reactivemongo.bson._ 25 | import reactivemongo.api.{ DB, QueryOpts } 26 | import reactivemongo.api.indexes.Index 27 | import reactivemongo.api.commands.{ GetLastError, WriteResult } 28 | import play.modules.reactivemongo.json._, collection.JSONCollection 29 | import reactivemongo.extensions.dao.{ Dao, LifeCycle, ReflexiveLifeCycle } 30 | import reactivemongo.extensions.json.dsl.JsonDsl._ 31 | 32 | import play.api.libs.json.{ 33 | Json, 34 | JsError, 35 | JsSuccess, 36 | JsObject, 37 | OFormat, 38 | OWrites, 39 | Reads, 40 | Writes 41 | } 42 | import play.api.libs.iteratee.{ Iteratee, Enumerator } 43 | 44 | /** 45 | * A DAO implementation that operates on JSONCollection using JsObject. 46 | * 47 | * To create a DAO for a concrete model extend this class. 48 | * 49 | * Below is a sample model. 50 | * {{{ 51 | * import reactivemongo.bson.BSONObjectID 52 | * import play.api.libs.json.Json 53 | * import play.modules.reactivemongo.json.BSONFormats._ 54 | * 55 | * case class Person( 56 | * _id: BSONObjectID = BSONObjectID.generate, 57 | * name: String, 58 | * surname: String, 59 | * age: Int) 60 | * 61 | * object Person { 62 | * implicit val personFormat = Json.format[Person] 63 | * } 64 | * 65 | * }}} 66 | * 67 | * To define a JsonDao for the Person model you just need to extend JsonDao. 68 | * 69 | * {{{ 70 | * import reactivemongo.api.{ MongoDriver, DB } 71 | * import reactivemongo.bson.BSONObjectID 72 | * import play.modules.reactivemongo.json.BSONFormats._ 73 | * import reactivemongo.extensions.json.dao.JsonDao 74 | * import scala.concurrent.ExecutionContext.Implicits.global 75 | * 76 | * 77 | * object MongoContext { 78 | * val driver = new MongoDriver 79 | * val connection = driver.connection(List("localhost")) 80 | * def db(): DB = connection("reactivemongo-extensions") 81 | * } 82 | * 83 | * object PersonDao extends JsonDao[Person, BSONObjectID](MongoContext.db, "persons") 84 | * }}} 85 | * 86 | * @param db A parameterless function returning a [[reactivemongo.api.DB]] instance. 87 | * @param collectionName Name of the collection this DAO is going to operate on. 88 | * @param lifeCycle [[reactivemongo.extensions.dao.LifeCycle]] for the Model type. 89 | * @tparam Model Type of the model that this DAO uses. 90 | * @tparam ID Type of the ID field of the model. 91 | */ 92 | abstract class JsonDao[Model: OFormat, ID: Writes](db: => DB, collectionName: String)(implicit lifeCycle: LifeCycle[Model, ID] = new ReflexiveLifeCycle[Model, ID], ec: ExecutionContext) 93 | extends Dao[JSONCollection, JsObject, Model, ID, OWrites]( 94 | db, collectionName) { 95 | 96 | def ensureIndexes()(implicit ec: ExecutionContext): Future[Traversable[Boolean]] = Future sequence { 97 | autoIndexes map { index => 98 | collection.indexesManager.ensure(index) 99 | } 100 | }.map { results => 101 | lifeCycle.ensuredIndexes() 102 | results 103 | } 104 | 105 | def listIndexes()(implicit ec: ExecutionContext): Future[List[Index]] = 106 | collection.indexesManager.list() 107 | 108 | def findOne(selector: JsObject = Json.obj())(implicit ec: ExecutionContext): Future[Option[Model]] = collection.find(selector).one[Model] 109 | 110 | def findById(id: ID)(implicit ec: ExecutionContext): Future[Option[Model]] = 111 | findOne($id(id)) 112 | 113 | def findByIds(ids: ID*)(implicit ec: ExecutionContext): Future[List[Model]] = 114 | findAll("_id" $in (ids: _*)) 115 | 116 | def find( 117 | selector: JsObject = Json.obj(), 118 | sort: JsObject = Json.obj("_id" -> 1), 119 | page: Int, 120 | pageSize: Int)(implicit ec: ExecutionContext): Future[List[Model]] = { 121 | val from = (page - 1) * pageSize 122 | collection 123 | .find(selector) 124 | .sort(sort) 125 | .options(QueryOpts(skipN = from, batchSizeN = pageSize)) 126 | .cursor[Model] 127 | .collect[List](pageSize) 128 | } 129 | 130 | def findAll( 131 | selector: JsObject = Json.obj(), 132 | sort: JsObject = Json.obj("_id" -> 1))(implicit ec: ExecutionContext): Future[List[Model]] = { 133 | collection.find(selector).sort(sort).cursor[Model].collect[List]() 134 | } 135 | 136 | @deprecated(since = "0.11.1", 137 | message = "Directly use [[findAndUpdate]] collection operation") 138 | def findAndUpdate( 139 | query: JsObject, 140 | update: JsObject, 141 | sort: JsObject = Json.obj(), 142 | fetchNewObject: Boolean = false, 143 | upsert: Boolean = false)(implicit ec: ExecutionContext): Future[Option[Model]] = collection.findAndUpdate( 144 | query, update, fetchNewObject, upsert).map(_.result[Model]) 145 | 146 | @deprecated(since = "0.11.1", 147 | message = "Directly use [[findAndRemove]] collection operation") 148 | def findAndRemove(query: JsObject, sort: JsObject = Json.obj())(implicit ec: ExecutionContext): Future[Option[Model]] = collection.findAndRemove( 149 | query, if (sort == BSONDocument.empty) None else Some(sort)). 150 | map(_.result[Model]) 151 | 152 | def findRandom(selector: JsObject = Json.obj())(implicit ec: ExecutionContext): Future[Option[Model]] = for { 153 | count <- count(selector) 154 | index = Random.nextInt(count) 155 | random <- collection.find(selector).options(QueryOpts(skipN = index, batchSizeN = 1)).one[Model] 156 | } yield random 157 | 158 | def insert(model: Model, writeConcern: GetLastError = defaultWriteConcern)(implicit ec: ExecutionContext): Future[WriteResult] = { 159 | val mappedModel = lifeCycle.prePersist(model) 160 | collection.insert(mappedModel, writeConcern) map { writeResult => 161 | lifeCycle.postPersist(mappedModel) 162 | writeResult 163 | } 164 | } 165 | 166 | private val (maxBulkSize, maxBsonSize): (Int, Int) = 167 | collection.db.connection.metadata.map { 168 | metadata => metadata.maxBulkSize -> metadata.maxBsonSize 169 | }.getOrElse[(Int, Int)](Int.MaxValue -> Int.MaxValue) 170 | 171 | def bulkInsert( 172 | documents: TraversableOnce[Model], 173 | bulkSize: Int = maxBulkSize, 174 | bulkByteSize: Int = maxBsonSize)(implicit ec: ExecutionContext): Future[Int] = { 175 | val mappedDocuments = documents.map(lifeCycle.prePersist) 176 | val writer = implicitly[OWrites[Model]] 177 | 178 | def go(docs: Traversable[Model]): Stream[JsObject] = docs.headOption match { 179 | case Some(doc) => writer.writes(doc) #:: go(docs.tail) 180 | case _ => Stream.Empty 181 | } 182 | 183 | collection.bulkInsert(go(mappedDocuments.toTraversable), 184 | true, defaultWriteConcern, bulkSize, bulkByteSize) map { result => 185 | mappedDocuments.map(lifeCycle.postPersist) 186 | result.n 187 | } 188 | } 189 | 190 | def update[U: OWrites]( 191 | selector: JsObject, 192 | update: U, 193 | writeConcern: GetLastError = defaultWriteConcern, 194 | upsert: Boolean = false, 195 | multi: Boolean = false)(implicit ec: ExecutionContext): Future[WriteResult] = collection.update(selector, update, writeConcern, upsert, multi) 196 | 197 | def updateById[U: OWrites]( 198 | id: ID, 199 | update: U, 200 | writeConcern: GetLastError = defaultWriteConcern)(implicit ec: ExecutionContext): Future[WriteResult] = collection.update($id(id), update, writeConcern) 201 | 202 | def save(model: Model, writeConcern: GetLastError = defaultWriteConcern)(implicit ec: ExecutionContext): Future[WriteResult] = { 203 | val mappedModel = lifeCycle.prePersist(model) 204 | collection.save(mappedModel, writeConcern) map { lastError => 205 | lifeCycle.postPersist(mappedModel) 206 | lastError 207 | } 208 | } 209 | 210 | def count(selector: JsObject = Json.obj())(implicit ec: ExecutionContext): Future[Int] = collection.count(Some(selector)) 211 | 212 | def drop()(implicit ec: ExecutionContext): Future[Unit] = collection.drop() 213 | 214 | def dropSync(timeout: Duration = 10 seconds)(implicit ec: ExecutionContext): Unit = Await.result(drop(), timeout) 215 | 216 | def removeById(id: ID, writeConcern: GetLastError = defaultWriteConcern)(implicit ec: ExecutionContext): Future[WriteResult] = { 217 | lifeCycle.preRemove(id) 218 | collection.remove($id(id), writeConcern = defaultWriteConcern) map { lastError => 219 | lifeCycle.postRemove(id) 220 | lastError 221 | } 222 | } 223 | 224 | def remove( 225 | query: JsObject, 226 | writeConcern: GetLastError = defaultWriteConcern, 227 | firstMatchOnly: Boolean = false)(implicit ec: ExecutionContext): Future[WriteResult] = { 228 | collection.remove(query, writeConcern, firstMatchOnly) 229 | } 230 | 231 | def removeAll(writeConcern: GetLastError = defaultWriteConcern)(implicit ec: ExecutionContext): Future[WriteResult] = { 232 | collection.remove(query = Json.obj(), writeConcern = writeConcern, firstMatchOnly = false) 233 | } 234 | 235 | def foreach( 236 | selector: JsObject = Json.obj(), 237 | sort: JsObject = Json.obj("_id" -> 1))(f: (Model) => Unit)(implicit ec: ExecutionContext): Future[Unit] = { 238 | collection.find(selector).sort(sort).cursor[Model] 239 | .enumerate() 240 | .apply(Iteratee.foreach(f)) 241 | .flatMap(i => i.run) 242 | } 243 | 244 | def fold[A]( 245 | selector: JsObject = Json.obj(), 246 | sort: JsObject = Json.obj("_id" -> 1), 247 | state: A)(f: (A, Model) => A)(implicit ec: ExecutionContext): Future[A] = { 248 | collection.find(selector).sort(sort).cursor[Model] 249 | .enumerate() 250 | .apply(Iteratee.fold(state)(f)) 251 | .flatMap(i => i.run) 252 | } 253 | 254 | ensureIndexes() 255 | } 256 | 257 | object JsonDao { 258 | def apply[Model: OFormat, ID: Writes](db: => DB, collectionName: String)( 259 | implicit lifeCycle: LifeCycle[Model, ID] = new ReflexiveLifeCycle[Model, ID], ec: ExecutionContext): JsonDao[Model, ID] = new JsonDao[Model, ID](db, collectionName) {} 260 | 261 | } 262 | -------------------------------------------------------------------------------- /json/src/main/scala/dao/JsonDaoBuilder.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.json.dao 18 | 19 | import play.api.libs.json.{ Writes, OFormat } 20 | import reactivemongo.api.DB 21 | import reactivemongo.extensions.dao.{ ReflexiveLifeCycle, LifeCycle } 22 | 23 | import scala.concurrent.ExecutionContext 24 | 25 | class JsonDaoBuilder[Model: OFormat, ID: Writes](db: => DB) { 26 | def apply(collectionName: String)( 27 | implicit lifeCycle: LifeCycle[Model, ID] = new ReflexiveLifeCycle[Model, ID], 28 | ec: ExecutionContext): JsonDao[Model, ID] = { 29 | JsonDao(db, collectionName) 30 | } 31 | } 32 | 33 | object JsonDaoBuilder { 34 | def apply[Model: OFormat, ID: Writes](db: => DB): JsonDaoBuilder[Model, ID] = { 35 | new JsonDaoBuilder[Model, ID](db) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /json/src/main/scala/dao/JsonFileDao.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import play.api.libs.json.{ JsObject, Json, JsValue, Writes } 20 | import reactivemongo.api.{ DB, DBMetaCommands } 21 | import reactivemongo.api.gridfs.IdProducer 22 | import reactivemongo.bson.BSONValue 23 | import reactivemongo.extensions.dao.FileDao.ReadFileWrapper 24 | 25 | import scala.concurrent.{ ExecutionContext, Future } 26 | 27 | /** 28 | * {{{ 29 | * import reactivemongo.extensions.dao.JsonFileDao, JsonFileDao._ 30 | * }}} 31 | */ 32 | abstract class JsonFileDao[Id <: JsValue: IdProducer](db: => DB with DBMetaCommands, collectionName: String)(implicit gridFsId: Id => BSONValue) 33 | extends FileDao[Id, JsObject](db, collectionName) { 34 | 35 | import play.modules.reactivemongo.json.JsObjectWriter 36 | 37 | def findById(id: Id)(implicit ec: ExecutionContext): ReadFileWrapper = 38 | findOne(Json.obj("_id" -> id)) 39 | } 40 | 41 | object JsonFileDao { 42 | import play.modules.reactivemongo.json.BSONFormats 43 | 44 | // !! unsafe 45 | implicit def defaultGridFSBSONId[T <: JsValue](json: T): BSONValue = 46 | BSONFormats.toBSON(json).getOrElse(sys.error(s"fails to convert $json")) 47 | } 48 | -------------------------------------------------------------------------------- /json/src/main/scala/fixtures/JsonFixtures.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.json.fixtures 18 | 19 | import scala.concurrent.{ Future, ExecutionContext } 20 | import reactivemongo.extensions.util.Logger 21 | import reactivemongo.extensions.fixtures.Fixtures 22 | import reactivemongo.api.DB 23 | import reactivemongo.api.commands.WriteResult 24 | import play.modules.reactivemongo.json._, collection.JSONCollection 25 | import play.api.libs.iteratee.Enumerator 26 | import play.api.libs.json.{ Json, JsObject } 27 | 28 | class JsonFixtures(db: => DB)(implicit ec: ExecutionContext) extends Fixtures[JsObject] { 29 | 30 | def map(document: JsObject): JsObject = document 31 | 32 | def bulkInsert(collectionName: String, documents: Stream[JsObject]): Future[Int] = db.collection[JSONCollection]( 33 | collectionName).bulkInsert(documents, ordered = true).map(_.n) 34 | 35 | def removeAll(collectionName: String): Future[WriteResult] = 36 | db.collection[JSONCollection](collectionName). 37 | remove(query = Json.obj(), firstMatchOnly = false) 38 | 39 | def drop(collectionName: String): Future[Unit] = 40 | db.collection[JSONCollection](collectionName).drop() 41 | 42 | } 43 | 44 | object JsonFixtures { 45 | def apply(db: DB)(implicit ec: ExecutionContext): JsonFixtures = new JsonFixtures(db) 46 | } 47 | 48 | -------------------------------------------------------------------------------- /json/src/test/resources/json/events.conf: -------------------------------------------------------------------------------- 1 | # Predefined reusable values 2 | _predef { 3 | location: { 4 | city: Ankara 5 | place: Salon 6 | } 7 | } 8 | 9 | 10 | # "events" collection 11 | events { 12 | 13 | event1 { 14 | _id: _id_event1 15 | title: Developer workshop 16 | organizer: ${persons.person1.fullname} 17 | location: ${_predef.location} 18 | } 19 | 20 | event2 { 21 | _id: _id_event2 22 | title: Some movie 23 | organizer: ${persons.person2.fullname} 24 | location: ${_predef.location} 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /json/src/test/resources/json/persons.conf: -------------------------------------------------------------------------------- 1 | # Predefined reusable values 2 | _predef { 3 | country: TC 4 | } 5 | 6 | 7 | # "persons" collection 8 | persons { 9 | 10 | person1 { 11 | _id: _id_person1 12 | name: Ali 13 | surname: Veli 14 | fullname: ${name} ${surname} 15 | age: 32 16 | country: ${_predef.country} 17 | } 18 | 19 | person2 { 20 | _id: _id_person2 21 | name: Haydar 22 | surname: Cabbar 23 | fullname: ${name} ${surname} 24 | age: ${person1.age} 25 | country: ${_predef.country} 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /json/src/test/scala/dao/CustomIdJsonDao.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.json.dao 18 | 19 | import scala.concurrent.ExecutionContext.Implicits.global 20 | import reactivemongo.extensions.json.model.CustomIdModel 21 | import reactivemongo.extensions.dao.MongoContext 22 | import reactivemongo.api.indexes.{ Index, IndexType } 23 | import reactivemongo.extensions.util.Misc.UUID 24 | 25 | class CustomIdJsonDao 26 | extends JsonDao[CustomIdModel, String](MongoContext.db, "dummy-" + UUID()) { 27 | 28 | override def autoIndexes = Seq( 29 | Index(Seq("name" -> IndexType.Ascending), unique = true, background = true), 30 | Index(Seq("age" -> IndexType.Ascending), background = true) 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /json/src/test/scala/dao/CustomIdJsonDaoSpec.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.json.dao 18 | 19 | import org.scalatest._ 20 | import org.scalatest.concurrent._ 21 | import org.scalatest.time.SpanSugar._ 22 | import play.api.libs.json.Json 23 | import play.modules.reactivemongo.json._ 24 | import reactivemongo.extensions.json.model.CustomIdModel 25 | import reactivemongo.extensions.json.dsl.JsonDsl._ 26 | import scala.concurrent.Future 27 | import scala.concurrent.ExecutionContext.Implicits.global 28 | 29 | class CustomIdJsonDaoSpec 30 | extends FlatSpec 31 | with Matchers 32 | with ScalaFutures 33 | with BeforeAndAfter 34 | with OneInstancePerTest { 35 | 36 | override implicit def patienceConfig = PatienceConfig(timeout = 20 seconds, interval = 1 seconds) 37 | 38 | val dao = new CustomIdJsonDao 39 | 40 | after { 41 | dao.dropSync() 42 | } 43 | 44 | "A CustomIdJsonDao" should "find document by id" in { 45 | val customIdModel = CustomIdModel(name = "foo", surname = "bar", age = 32) 46 | 47 | val futureResult = for { 48 | insertResult <- dao.insert(customIdModel) 49 | maybeCustomIdModel <- dao.findById(customIdModel._id) 50 | } yield maybeCustomIdModel 51 | 52 | whenReady(futureResult) { maybeCustomIdModel => 53 | maybeCustomIdModel should be('defined) 54 | maybeCustomIdModel.get._id shouldBe customIdModel._id 55 | maybeCustomIdModel.get.age shouldBe customIdModel.age 56 | } 57 | } 58 | 59 | it should "find documents by ids" in { 60 | val customIdModels = CustomIdModel.random(100) 61 | 62 | val futureResult = for { 63 | insertResult <- dao.bulkInsert(customIdModels) 64 | models <- dao.findByIds(customIdModels.drop(5).map(_._id): _*) 65 | } yield models 66 | 67 | whenReady(futureResult) { models => 68 | models should have size 95 69 | } 70 | } 71 | 72 | it should "update document by id" in { 73 | val customIdModel = CustomIdModel(name = "foo", surname = "bar", age = 32) 74 | val update = $set("age" -> 64) 75 | 76 | val futureResult = for { 77 | insert <- dao.insert(customIdModel) 78 | update <- dao.updateById(customIdModel._id, update) 79 | updatedMaybeCustomIdModel <- dao.findById(customIdModel._id) 80 | } yield updatedMaybeCustomIdModel 81 | 82 | whenReady(futureResult) { updatedMaybeCustomIdModel => 83 | updatedMaybeCustomIdModel should be('defined) 84 | val updatedCustomIdModel = updatedMaybeCustomIdModel.get 85 | updatedCustomIdModel._id shouldBe customIdModel._id 86 | updatedCustomIdModel.age shouldBe 64 87 | } 88 | } 89 | 90 | it should "update the whole document by id" in { 91 | val customIdModel = CustomIdModel(name = "foo", surname = "bar", age = 32) 92 | val update = customIdModel.copy(age = 64) 93 | 94 | val futureResult = for { 95 | insert <- dao.insert(customIdModel) 96 | update <- dao.updateById(customIdModel._id, update) 97 | updatedMaybeCustomIdModel <- dao.findById(customIdModel._id) 98 | } yield updatedMaybeCustomIdModel 99 | 100 | whenReady(futureResult) { updatedMaybeCustomIdModel => 101 | updatedMaybeCustomIdModel should be('defined) 102 | val updatedCustomIdModel = updatedMaybeCustomIdModel.get 103 | updatedCustomIdModel._id shouldBe customIdModel._id 104 | updatedCustomIdModel.age shouldBe 64 105 | } 106 | } 107 | 108 | it should "ensure indexes" in { 109 | val futureIndexes = Future { 110 | // Give some time for indexes to be ensured 111 | Thread.sleep(2000) 112 | } flatMap { _ => 113 | dao.listIndexes() 114 | } 115 | 116 | whenReady(futureIndexes) { indexes => 117 | indexes should have size 3 // including _id 118 | } 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /json/src/test/scala/dao/DummyJsonDao.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.json.dao 18 | 19 | import scala.concurrent.ExecutionContext.Implicits.global 20 | import reactivemongo.extensions.json.model.DummyModel 21 | import reactivemongo.extensions.dao.MongoContext 22 | import reactivemongo.api.indexes.{ Index, IndexType } 23 | import reactivemongo.bson.BSONObjectID 24 | import play.modules.reactivemongo.json.BSONFormats._ 25 | import reactivemongo.extensions.util.Misc.UUID 26 | 27 | class DummyJsonDao extends { 28 | override val autoIndexes = Seq( 29 | Index(Seq("name" -> IndexType.Ascending), unique = true, background = true), 30 | Index(Seq("age" -> IndexType.Ascending), background = true) 31 | ) 32 | } with JsonDao[DummyModel, BSONObjectID](MongoContext.db, "dummy-" + UUID()) 33 | -------------------------------------------------------------------------------- /json/src/test/scala/dao/DynamicJsonDaoSpec.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.json.dao 18 | 19 | import org.scalatest._ 20 | import org.scalatest.concurrent.{ ScalaFutures } 21 | import org.scalatest.time.{ Span, Seconds } 22 | import play.api.libs.json.{ 23 | OFormat, 24 | JsError, 25 | Json, 26 | JsObject, 27 | JsResult, 28 | JsSuccess, 29 | JsValue 30 | } 31 | import reactivemongo.bson.BSONObjectID 32 | import reactivemongo.extensions.dao.MongoContext 33 | import reactivemongo.extensions.json.dsl.JsonDsl._ 34 | import reactivemongo.extensions.util.Logger 35 | import reactivemongo.extensions.Implicits._ 36 | import scala.concurrent.{ Future, Await } 37 | import scala.concurrent.ExecutionContext.Implicits.global 38 | 39 | class DynamicJsonDaoSpec 40 | extends FlatSpec 41 | with Matchers 42 | with ScalaFutures 43 | with BeforeAndAfter 44 | with OneInstancePerTest { 45 | 46 | import play.modules.reactivemongo.json._ 47 | 48 | override implicit def patienceConfig = PatienceConfig(timeout = Span(20, Seconds), interval = Span(1, Seconds)) 49 | 50 | implicit object JsObjectFormat extends OFormat[JsObject] { 51 | def reads(json: JsValue): JsResult[JsObject] = json match { 52 | case obj: JsObject => JsSuccess(obj) 53 | case _ => JsError("error.jsobject.expected") 54 | } 55 | 56 | def writes(obj: JsObject): JsObject = obj 57 | } 58 | 59 | val builder = JsonDaoBuilder[JsObject, BSONObjectID](MongoContext.db) 60 | 61 | before { 62 | import scala.concurrent.duration._ 63 | Await.ready(builder("collection1").removeAll(), 10 seconds) 64 | Await.ready(builder("collection2").removeAll(), 10 seconds) 65 | } 66 | 67 | "A DynamicJsonDao" should "use different collections" in { 68 | val dao1 = builder("collection1") 69 | val dao2 = builder("collection2") 70 | 71 | val futureResult = for { 72 | insertResult1 <- dao1.insert($doc("name" -> "ali", "surname" -> "veli")) 73 | insertResult2 <- dao2.insert($doc("name" -> "haydar", "surname" -> "cabbar", "age" -> 18)) 74 | result1 <- ~dao1.findOne("name" $eq "ali") 75 | result2 <- ~dao2.findOne("name" $eq "haydar") 76 | insertResult12 <- dao1.insert(result2) 77 | count1 <- dao1.count() 78 | count2 <- dao2.count() 79 | } yield (result1, result2, count1, count2) 80 | 81 | whenReady(futureResult) { 82 | case (result1, result2, count1, count2) => 83 | (result1 \ "surname").as[String] shouldBe "veli" 84 | (result2 \ "surname").as[String] shouldBe "cabbar" 85 | count1 shouldBe 2 86 | count2 shouldBe 1 87 | } 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /json/src/test/scala/dao/EventJsonDao.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.json.dao 18 | 19 | import scala.concurrent.Future 20 | import scala.concurrent.ExecutionContext.Implicits.global 21 | import reactivemongo.extensions.json.model.Event 22 | import reactivemongo.extensions.json.dsl.JsonDsl 23 | import reactivemongo.api.DB 24 | 25 | class EventJsonDao(_db: DB) 26 | extends JsonDao[Event, String](_db, "events") 27 | with JsonDsl { 28 | 29 | def findByTitle(title: String): Future[Option[Event]] = { 30 | findOne("title" $eq title) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /json/src/test/scala/dao/JsonFileDaoSpec.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.dao 18 | 19 | import java.io.ByteArrayOutputStream 20 | 21 | import org.scalatest._ 22 | import org.scalatest.concurrent._ 23 | import org.scalatest.time.{ Seconds, Span } 24 | import play.api.libs.iteratee.{ Iteratee, Enumerator } 25 | import play.api.libs.json.{ Json, JsObject } 26 | import reactivemongo.api.gridfs.Implicits.DefaultReadFileReader 27 | import reactivemongo.bson.BSONObjectID 28 | 29 | import scala.concurrent.ExecutionContext.Implicits.global 30 | 31 | class JsonFileDaoSpec 32 | extends FlatSpec 33 | with Matchers 34 | with ScalaFutures 35 | with BeforeAndAfter 36 | with OneInstancePerTest { 37 | 38 | import play.modules.reactivemongo.json._ 39 | 40 | override implicit def patienceConfig = PatienceConfig(timeout = Span(20, Seconds), interval = Span(1, Seconds)) 41 | 42 | import JsonFileDao._ 43 | 44 | val dao = new JsonFileDao[JsObject](MongoContext.db, "json-files") {} 45 | 46 | "A JsonFileDao" should "save and remove file" in { 47 | val enumerator = Enumerator.fromStream(getClass.getResourceAsStream("/whyfp90.pdf")) 48 | 49 | val result = for { 50 | save <- dao.save(enumerator, filename = "whyfp90.pdf", contentType = "application/pdf") 51 | id = BSONFormats.toJSON(save.id).asInstanceOf[JsObject] 52 | findBefore <- dao.findById(id) 53 | remove <- dao.removeById(id) 54 | findAfter <- dao.findById(id) 55 | } yield (id, findBefore, findAfter) 56 | 57 | whenReady(result) { 58 | case (id, findBefore, findAfter) => 59 | import org.scalatest.OptionValues._ 60 | BSONFormats.toJSON(findBefore.value.id) should be(id) 61 | findBefore.value.length should be(200007) 62 | findAfter should be('empty) 63 | } 64 | } 65 | 66 | it should "find file by name" in { 67 | val enumerator = Enumerator.fromStream(getClass.getResourceAsStream("/whyfp90.pdf")) 68 | 69 | val result = for { 70 | save <- dao.save(enumerator, filename = "whyfp90.pdf", contentType = "application/pdf") 71 | id = BSONFormats.toJSON(save.id).asInstanceOf[JsObject] 72 | find <- dao.findOne(Json.obj("filename" -> save.filename)) 73 | remove <- dao.removeById(id) 74 | } yield (id, find) 75 | 76 | whenReady(result) { 77 | case (id, find) => 78 | import org.scalatest.OptionValues._ 79 | BSONFormats.toJSON(find.value.id) should be(id) 80 | } 81 | } 82 | 83 | it should "enumerate one" in { 84 | val enumerator = Enumerator.fromStream(getClass.getResourceAsStream("/whyfp90.pdf")) 85 | 86 | val length = Iteratee.fold(0) { (state: Int, bytes: Array[Byte]) => 87 | state + bytes.size 88 | } 89 | 90 | val result = for { 91 | save <- dao.save(enumerator, filename = "whyfp90.pdf", contentType = "application/pdf") 92 | id = BSONFormats.toJSON(save.id).asInstanceOf[JsObject] 93 | enumerator <- dao.findOne(Json.obj("filename" -> save.filename)).enumerate 94 | len <- enumerator.get |>>> length 95 | remove <- dao.removeById(id) 96 | } yield (len) 97 | 98 | whenReady(result) { length => 99 | length shouldBe 200007 100 | } 101 | } 102 | 103 | it should "enumerate by id" in { 104 | val enumerator = Enumerator.fromStream(getClass.getResourceAsStream("/whyfp90.pdf")) 105 | 106 | val length = Iteratee.fold(0) { (state: Int, bytes: Array[Byte]) => 107 | state + bytes.size 108 | } 109 | 110 | val result = for { 111 | save <- dao.save(enumerator, filename = "whyfp90.pdf", contentType = "application/pdf") 112 | id = BSONFormats.toJSON(save.id).asInstanceOf[JsObject] 113 | enumerator <- dao.findById(id).enumerate 114 | len <- enumerator.get |>>> length 115 | remove <- dao.removeById(id) 116 | } yield (len) 117 | 118 | whenReady(result) { length => 119 | length shouldBe 200007 120 | } 121 | } 122 | 123 | it should "read one to outputstream" in { 124 | val enumerator = Enumerator.fromStream(getClass.getResourceAsStream("/whyfp90.pdf")) 125 | val out = new ByteArrayOutputStream 126 | 127 | val result = for { 128 | save <- dao.save(enumerator, filename = "whyfp90.pdf", contentType = "application/pdf") 129 | id = BSONFormats.toJSON(save.id).asInstanceOf[JsObject] 130 | read <- dao.findOne(Json.obj("filename" -> save.filename)).read(out) 131 | remove <- dao.removeById(id) 132 | } yield read 133 | 134 | whenReady(result) { read => 135 | read shouldBe ('defined) 136 | out.size() shouldBe 200007 137 | } 138 | } 139 | 140 | it should "read by id to outputstream" in { 141 | val enumerator = Enumerator.fromStream(getClass.getResourceAsStream("/whyfp90.pdf")) 142 | val out = new ByteArrayOutputStream 143 | 144 | val result = for { 145 | save <- dao.save(enumerator, filename = "whyfp90.pdf", contentType = "application/pdf") 146 | id = BSONFormats.toJSON(save.id).asInstanceOf[JsObject] 147 | read <- dao.findById(id).read(out) 148 | remove <- dao.removeById(id) 149 | } yield read 150 | 151 | whenReady(result) { read => 152 | read shouldBe ('defined) 153 | out.size() shouldBe 200007 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /json/src/test/scala/dao/PersonJsonDao.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.json.dao 18 | 19 | import scala.concurrent.ExecutionContext.Implicits.global 20 | import reactivemongo.extensions.json.model.Person 21 | import reactivemongo.api.DB 22 | import scala.concurrent.Future 23 | import reactivemongo.extensions.json.dsl.JsonDsl 24 | 25 | class PersonJsonDao(_db: DB) 26 | extends JsonDao[Person, String](_db, "persons") 27 | with JsonDsl { 28 | 29 | def findByName(name: String): Future[Option[Person]] = { 30 | findOne("name" $eq name) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /json/src/test/scala/dao/TemporalModelJsonDao.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.json.dao 18 | 19 | import scala.concurrent.ExecutionContext.Implicits.global 20 | import reactivemongo.extensions.dao.MongoContext 21 | import reactivemongo.extensions.json.model.TemporalModel 22 | import reactivemongo.extensions.util.Misc.UUID 23 | import reactivemongo.bson.BSONObjectID 24 | import play.modules.reactivemongo.json.BSONFormats._ 25 | 26 | class TemporalModelJsonDao 27 | extends JsonDao[TemporalModel, BSONObjectID](MongoContext.db, "temporal_model_" + UUID()) 28 | 29 | -------------------------------------------------------------------------------- /json/src/test/scala/dao/TemporalModelJsonDaoSpec.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.json.dao 18 | 19 | import org.scalatest._ 20 | import org.scalatest.concurrent._ 21 | import org.scalatest.time.SpanSugar._ 22 | import reactivemongo.bson._ 23 | import reactivemongo.extensions.json.model.TemporalModel 24 | import scala.concurrent.Future 25 | import scala.concurrent.ExecutionContext.Implicits.global 26 | 27 | class TemporalModelJsonDaoSpec 28 | extends FlatSpec 29 | with Matchers 30 | with ScalaFutures 31 | with BeforeAndAfter 32 | with OneInstancePerTest { 33 | 34 | override implicit def patienceConfig = PatienceConfig(timeout = 20 seconds, interval = 1 seconds) 35 | 36 | val dao = new TemporalModelJsonDao 37 | 38 | after { 39 | dao.dropSync() 40 | } 41 | 42 | "A TemporalModelJsonDao" should "update updateAt" in { 43 | val temporalModel = TemporalModel(name = "foo", surname = "bar") 44 | 45 | val futureResult = for { 46 | insertResult <- dao.insert(temporalModel) 47 | maybeTemporalModel <- dao.findById(temporalModel._id) 48 | } yield maybeTemporalModel 49 | 50 | whenReady(futureResult) { maybeTemporalModel => 51 | maybeTemporalModel should be('defined) 52 | maybeTemporalModel.get._id shouldBe temporalModel._id 53 | maybeTemporalModel.get.name shouldBe temporalModel.name 54 | maybeTemporalModel.get.surname shouldBe temporalModel.surname 55 | maybeTemporalModel.get.createdAt shouldBe temporalModel.createdAt 56 | maybeTemporalModel.get.updatedAt.isAfter(temporalModel.updatedAt) shouldBe true 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /json/src/test/scala/fixtures/JsonFixturesSpec.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.json.fixtures 18 | 19 | import org.scalatest._ 20 | import org.scalatest.concurrent._ 21 | import org.scalatest.time.{ Span, Seconds } 22 | import reactivemongo.extensions.util.Logger 23 | import reactivemongo.extensions.dao.MongoContext 24 | import reactivemongo.extensions.json.dao.{ PersonJsonDao, EventJsonDao } 25 | import reactivemongo.extensions.Implicits._ 26 | import scala.concurrent.ExecutionContext.Implicits.global 27 | 28 | class JsonFixturesSpec extends FlatSpec with Matchers with ScalaFutures with BeforeAndAfter { 29 | 30 | override implicit def patienceConfig = PatienceConfig(timeout = Span(20, Seconds), interval = Span(1, Seconds)) 31 | 32 | val db = MongoContext.randomDb 33 | val fixtures = JsonFixtures(db) 34 | val personDao = new PersonJsonDao(db) 35 | val eventDao = new EventJsonDao(db) 36 | 37 | after { 38 | db.drop() 39 | } 40 | 41 | "A JsonFixtures" should "load persons" in { 42 | val futureCount = for { 43 | remove <- fixtures.removeAll("json/persons.conf") 44 | beforeCount <- personDao.count() 45 | insert <- fixtures.load("json/persons.conf") 46 | afterCount <- personDao.count() 47 | person1 <- ~personDao.findByName("Ali") 48 | } yield (beforeCount, afterCount, person1) 49 | 50 | whenReady(futureCount) { 51 | case (beforeCount, afterCount, person1) => 52 | beforeCount shouldBe 0 53 | afterCount shouldBe 2 54 | person1.fullname shouldBe "Ali Veli" 55 | } 56 | } 57 | 58 | it should "load persons and events" in { 59 | val futureCount = for { 60 | remove <- fixtures.removeAll("json/persons.conf", "json/events.conf") 61 | beforeCount <- eventDao.count() 62 | insert <- fixtures.load("json/persons.conf", "json/events.conf") 63 | afterCount <- eventDao.count() 64 | event2 <- ~eventDao.findByTitle("Some movie") 65 | } yield (beforeCount, afterCount, event2) 66 | 67 | whenReady(futureCount) { 68 | case (beforeCount, afterCount, event2) => 69 | beforeCount shouldBe 0 70 | afterCount shouldBe 2 71 | event2.organizer shouldBe "Haydar Cabbar" 72 | event2.location.city shouldBe "Ankara" 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /json/src/test/scala/model/CustomIdModel.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.json.model 18 | 19 | import reactivemongo.bson._ 20 | import reactivemongo.extensions.dao.Handlers._ 21 | import play.api.libs.json.Json 22 | import play.modules.reactivemongo.json.BSONFormats._ 23 | import reactivemongo.extensions.util.Misc.UUID 24 | 25 | case class CustomIdModel( 26 | _id: String = UUID(), 27 | name: String, 28 | surname: String, 29 | age: Int) 30 | 31 | object CustomIdModel { 32 | implicit val customIdModelHandler = Macros.handler[CustomIdModel] 33 | implicit val customIdModelFormat = Json.format[CustomIdModel] 34 | 35 | def random(n: Int): Seq[CustomIdModel] = 1 to n map { index => 36 | CustomIdModel(name = s"name$index", surname = "surname$index", age = index) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /json/src/test/scala/model/DummyModel.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.json.model 18 | 19 | import reactivemongo.bson._ 20 | import reactivemongo.extensions.dao.Handlers._ 21 | import play.api.libs.json.Json 22 | import play.modules.reactivemongo.json.BSONFormats._ 23 | 24 | case class DummyModel( 25 | _id: BSONObjectID = BSONObjectID.generate, 26 | name: String, 27 | surname: String, 28 | age: Int) 29 | 30 | object DummyModel { 31 | implicit val dummyModelHandler = Macros.handler[DummyModel] 32 | implicit val dummyModelFormat = Json.format[DummyModel] 33 | 34 | def random(n: Int): Seq[DummyModel] = 1 to n map { index => 35 | DummyModel(name = s"name$index", surname = "surname$index", age = index) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /json/src/test/scala/model/Event.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.json.model 18 | 19 | import reactivemongo.bson._ 20 | import reactivemongo.extensions.dao.Handlers._ 21 | import play.api.libs.json.Json 22 | import play.modules.reactivemongo.json.BSONFormats._ 23 | 24 | case class Event( 25 | _id: String, 26 | title: String, 27 | organizer: String, 28 | location: Location) 29 | 30 | case class Location(city: String, place: String) 31 | 32 | object Event { 33 | implicit val locationFormat = Json.format[Location] 34 | implicit val eventFormat = Json.format[Event] 35 | } 36 | -------------------------------------------------------------------------------- /json/src/test/scala/model/MapModel.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.json.model 18 | 19 | import reactivemongo.bson._ 20 | import reactivemongo.extensions.dao.Handlers._ 21 | import play.api.libs.json.Json 22 | import play.modules.reactivemongo.json.BSONFormats._ 23 | 24 | case class MapModel( 25 | _id: BSONObjectID = BSONObjectID.generate, 26 | data: Map[String, Int]) 27 | 28 | object MapModel { 29 | implicit val customIdModelFormat = Json.format[MapModel] 30 | 31 | def random(n: Int): Seq[MapModel] = 1 to n map { index => 32 | MapModel(data = Map("count" -> n)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /json/src/test/scala/model/Person.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.json.model 18 | 19 | import play.api.libs.json.Json 20 | 21 | case class Person( 22 | _id: String, 23 | name: String, 24 | surname: String, 25 | fullname: String, 26 | age: Int, 27 | country: String) 28 | 29 | object Person { 30 | implicit val personFormat = Json.format[Person] 31 | } 32 | -------------------------------------------------------------------------------- /json/src/test/scala/model/TemporalModel.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014 Fehmi Can Saglam (@fehmicans) and contributors. 2 | // See the LICENCE.txt file distributed with this work for additional 3 | // information regarding copyright ownership. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package reactivemongo.extensions.json.model 18 | 19 | import reactivemongo.bson.BSONObjectID 20 | import reactivemongo.extensions.dao.LifeCycle 21 | import reactivemongo.extensions.util.Logger 22 | import play.api.libs.json.Json 23 | import play.modules.reactivemongo.json.BSONFormats._ 24 | import org.joda.time.DateTime 25 | 26 | case class TemporalModel( 27 | _id: BSONObjectID = BSONObjectID.generate, 28 | name: String, 29 | surname: String, 30 | createdAt: DateTime = DateTime.now, 31 | updatedAt: DateTime = DateTime.now) 32 | 33 | object TemporalModel { 34 | implicit val temporalModelFormat = Json.format[TemporalModel] 35 | 36 | implicit object TemporalModelLifeCycle extends LifeCycle[TemporalModel, BSONObjectID] { 37 | def prePersist(model: TemporalModel): TemporalModel = { 38 | Logger.debug(s"prePersist $model") 39 | model.copy(updatedAt = DateTime.now) 40 | } 41 | 42 | def postPersist(model: TemporalModel): Unit = { 43 | Logger.debug(s"postPersist $model") 44 | } 45 | 46 | def preRemove(id: BSONObjectID): Unit = { 47 | Logger.debug(s"preRemove $id") 48 | } 49 | 50 | def postRemove(id: BSONObjectID): Unit = { 51 | Logger.debug(s"postRemove $id") 52 | } 53 | 54 | def ensuredIndexes(): Unit = { 55 | Logger.debug("ensuredIndexes") 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /project/Colors.scala: -------------------------------------------------------------------------------- 1 | object Colors { 2 | 3 | import scala.Console._ 4 | 5 | lazy val isANSISupported = { 6 | Option(System.getProperty("sbt.log.noformat")).map(_ != "true").orElse { 7 | Option(System.getProperty("os.name")) 8 | .map(_.toLowerCase) 9 | .filter(_.contains("windows")) 10 | .map(_ => false) 11 | }.getOrElse(true) 12 | } 13 | 14 | def red(str: String): String = if (isANSISupported) (RED + str + RESET) else str 15 | def blue(str: String): String = if (isANSISupported) (BLUE + str + RESET) else str 16 | def cyan(str: String): String = if (isANSISupported) (CYAN + str + RESET) else str 17 | def green(str: String): String = if (isANSISupported) (GREEN + str + RESET) else str 18 | def magenta(str: String): String = if (isANSISupported) (MAGENTA + str + RESET) else str 19 | def white(str: String): String = if (isANSISupported) (WHITE + str + RESET) else str 20 | def black(str: String): String = if (isANSISupported) (BLACK + str + RESET) else str 21 | def yellow(str: String): String = if (isANSISupported) (YELLOW + str + RESET) else str 22 | 23 | } 24 | -------------------------------------------------------------------------------- /project/Common.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | 4 | object Common { 5 | lazy val prompt = { state: State => 6 | val extracted = Project.extract(state) 7 | import extracted._ 8 | 9 | (name in currentRef get structure.data).map { name => 10 | "[" + Colors.blue(name) + "] $ " 11 | }.getOrElse("> ") 12 | } 13 | 14 | val playVersion = "2.4.2" 15 | val reactiveMongoVersion = "0.11.7" 16 | val playReactiveMongoVersion = "0.11.7.play24" 17 | } 18 | -------------------------------------------------------------------------------- /project/Travis.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | 4 | object Travis { 5 | val travisSnapshotBranches = SettingKey[Seq[String]]("branches that can be published on sonatype") 6 | 7 | val travisCommand = Command.command("publishSnapshotsFromTravis") { state => 8 | val extracted = Project extract state 9 | import extracted._ 10 | 11 | val thisRef = extracted.get(thisProjectRef) 12 | 13 | val isSnapshot = getOpt(version).exists(_.endsWith("SNAPSHOT")) 14 | val isTravisEnabled = sys.env.get("TRAVIS").exists(_ == "true") 15 | val isNotPR = sys.env.get("TRAVIS_PULL_REQUEST").exists(_ == "false") 16 | val isBranchAcceptable = sys.env.get("TRAVIS_BRANCH").exists(branch => getOpt(travisSnapshotBranches).exists(_.contains(branch))) 17 | 18 | if(isSnapshot && isTravisEnabled && isNotPR && isBranchAcceptable) { 19 | 20 | println(s"publishing $thisRef from travis...") 21 | 22 | val newState = 23 | append( 24 | Seq( 25 | publishTo := Some("Sonatype Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/"), 26 | credentials := Seq(Credentials( 27 | "Sonatype Nexus Repository Manager", 28 | "oss.sonatype.org", 29 | sys.env.get("SONATYPE_USER").getOrElse(throw new RuntimeException("no SONATYPE_USER defined")), 30 | sys.env.get("SONATYPE_PASSWORD").getOrElse(throw new RuntimeException("no SONATYPE_PASSWORD defined")) 31 | ))), 32 | state 33 | ) 34 | 35 | runTask(publish in thisRef, newState) 36 | 37 | println(s"published $thisRef from travis") 38 | } else { 39 | println(s"not publishing $thisRef to Sonatype : isSnapshot=$isSnapshot, isTravisEnabled=$isTravisEnabled, isNotPR=$isNotPR, isBranchAcceptable=$isBranchAcceptable") 40 | } 41 | 42 | state 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.2 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.3.0") 2 | 3 | addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.3.2") 4 | 5 | addSbtPlugin("com.orrsella" % "sbt-sublime" % "1.0.9") 6 | 7 | addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.3.1") 8 | -------------------------------------------------------------------------------- /samples/build.sbt: -------------------------------------------------------------------------------- 1 | name := "reactivemongo-extensions-samples" 2 | -------------------------------------------------------------------------------- /samples/src/test/scala/dsl/criteria/TypedCriteriaSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Steve Vickers 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * Created on: Jun 22, 2014 17 | */ 18 | package reactivemongo.extensions.samples.dsl.criteria 19 | 20 | import org.scalatest._ 21 | import org.scalatest.junit.JUnitRunner 22 | 23 | import reactivemongo.bson._ 24 | import reactivemongo.extensions.dsl.criteria._ 25 | 26 | trait Company { 27 | val name: String; 28 | val employees: Int; 29 | } 30 | 31 | case class Person(val firstName: String, val lastName: String, age: Int) 32 | 33 | /** 34 | * The '''TypedCriteriaSpec''' type verifies the expected behaviour of the 35 | * [[reactivemongo.extensions.dsl.criteria.Typed]] Criteria DSL. 36 | * 37 | * @author svickers 38 | * 39 | */ 40 | class TypedCriteriaSpec 41 | extends FlatSpec 42 | with Matchers { 43 | /// Class Imports 44 | import Typed._ 45 | 46 | "Typed criteria" should "produce a Term" in 47 | { 48 | val t = criteria[Person].firstName; 49 | 50 | t.getClass shouldBe (classOf[Term[_]]); 51 | } 52 | 53 | it should "produce a Term with the given property name" in 54 | { 55 | val t = criteria[Person].lastName; 56 | 57 | t match { 58 | case Term(propertyName) => 59 | propertyName shouldBe ("lastName"); 60 | } 61 | } 62 | 63 | it should "work with valid expressions" in 64 | { 65 | (criteria[Person].firstName =~ "sample regex") shouldBe ( 66 | Expression( 67 | Some("firstName"), 68 | ("$regex", BSONRegex("sample regex", "")) 69 | ) 70 | ); 71 | } 72 | 73 | it should "support trait-based expressions" in 74 | { 75 | val query = criteria[Company].employees === 42 || 76 | criteria[Company].employees < 10; 77 | 78 | BSONDocument.pretty(query) shouldBe ( 79 | BSONDocument.pretty( 80 | BSONDocument( 81 | "$or" -> 82 | BSONArray( 83 | BSONDocument( 84 | "employees" -> BSONInteger(42) 85 | ), 86 | BSONDocument( 87 | "employees" -> 88 | BSONDocument("$lt" -> 10) 89 | ) 90 | ) 91 | ) 92 | ) 93 | ); 94 | } 95 | } 96 | 97 | --------------------------------------------------------------------------------