├── .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 |
--------------------------------------------------------------------------------