├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── build.sbt
├── cla
├── cla-corporate.doc
├── cla-corporate.pdf
├── cla-individual.doc
└── cla-individual.pdf
├── cloudant-supported-features.md
├── code-style
└── intellij-code-style.xml
├── couchdb-supported-features.md
├── examples
└── src
│ └── main
│ ├── resources
│ └── logback.xml
│ └── scala
│ └── com
│ └── ibm
│ └── couchdb
│ └── examples
│ └── Basic.scala
├── project
└── plugins.sbt
├── scalastyle-config.xml
└── src
├── main
└── scala
│ └── com
│ └── ibm
│ └── couchdb
│ ├── CouchDb.scala
│ ├── Lenses.scala
│ ├── Model.scala
│ ├── Req.scala
│ ├── Res.scala
│ ├── TypeMapping.scala
│ ├── api
│ ├── Databases.scala
│ ├── Design.scala
│ ├── Documents.scala
│ ├── Query.scala
│ ├── Server.scala
│ └── builders
│ │ ├── GetDocumentQueryBuilder.scala
│ │ ├── GetManyDocumentsQueryBuilder.scala
│ │ ├── ListQueryBuilder.scala
│ │ ├── QueryOps.scala
│ │ ├── QueryStrategy.scala
│ │ ├── ShowQueryBuilder.scala
│ │ └── ViewQueryBuilder.scala
│ ├── core
│ └── Client.scala
│ ├── implicits
│ ├── TaskImplicits.scala
│ └── UpickleImplicits.scala
│ └── package.scala
└── test
├── resources
└── logback-test.xml
└── scala
└── com
└── ibm
└── couchdb
├── BasicAuthSpec.scala
├── CouchDbSpec.scala
├── api
├── DatabasesSpec.scala
├── DesignSpec.scala
├── DocumentsSpec.scala
├── QueryListSpec.scala
├── QueryShowSpec.scala
├── QueryTemporaryViewSpec.scala
├── QueryViewSpec.scala
└── ServerSpec.scala
├── implicits
├── TaskImplicitsSpec.scala
└── UpickleImplicitsSpec.scala
└── spec
├── CouchDbSpecification.scala
├── Fixtures.scala
└── SpecConfig.scala
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | target
3 | project/target
4 | project/project/target
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: scala
2 | scala:
3 | - 2.11.8
4 | jdk:
5 | - oraclejdk8
6 | services:
7 | - couchdb
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CouchDB-Scala
2 |
3 | [](https://travis-ci.org/beloglazov/couchdb-scala)
4 | [](https://gitter.im/beloglazov/couchdb-scala?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
5 | [](https://maven-badges.herokuapp.com/maven-central/com.ibm/couchdb-scala_2.11)
6 | [](https://waffle.io/beloglazov/couchdb-scala)
7 |
8 |
9 | This is a purely functional Scala client for
10 | [CouchDB](http://couchdb.apache.org/). The design goals are compositionality,
11 | expressiveness, type-safety, and ease of use.
12 |
13 | It's based on these awesome libraries:
14 | [Scalaz](https://github.com/scalaz/scalaz),
15 | [Http4s](https://github.com/http4s/http4s),
16 | [uPickle](https://github.com/lihaoyi/upickle-pprint), and
17 | [Monocle](https://github.com/julien-truffaut/Monocle).
18 |
19 |
20 | ## Getting started
21 |
22 | Add the following dependency to your SBT config:
23 |
24 | ```Scala
25 | libraryDependencies += "com.ibm" %% "couchdb-scala" % "0.7.2"
26 | ```
27 |
28 |
29 | ## Tutorial
30 |
31 | This Scala client tries to stay as close to the native CouchDB API as possible,
32 | while adding type-safety and automatic serialization/deserialization of Scala
33 | objects to and from JSON using uPickle. The best way to get up to speed with the
34 | client is to first obtain a good understanding of how CouchDB works and its
35 | native API. Some good resources to learn CouchDB are:
36 |
37 | - [CouchDB: The Definitive Guide](http://guide.couchdb.org/)
38 | - [CouchDB Documentation](http://docs.couchdb.org/en/)
39 |
40 | To get started, add the following import to your Scala file (or just start the
41 | SBT console with `sbt console`, which automatically adds the required imports):
42 |
43 | ```Scala
44 | import com.ibm.couchdb._
45 | ```
46 |
47 | Then, you need to create a client instance by passing in the IP address or host
48 | name of the CouchDB server, port number, and optionally the scheme (which
49 | defaults to `http`):
50 |
51 | ```Scala
52 | val couch = CouchDb("127.0.0.1", 5984)
53 | ```
54 |
55 | Through this object, you get access to the following CouchDB API sections:
56 |
57 | - Server: server-level operations
58 | - Databases: operations on databases
59 | - Design: operations for creating and managing design documents
60 | - Documents: creating, modifying, and querying documents and attachments
61 | - Query: querying views, shows, and lists
62 |
63 |
64 | ### Server API
65 |
66 | The server API section provides only 3 operations: getting the server info,
67 | which is equivalent to making a GET request to the `/` resource of the CouchDB
68 | server; generating a UUID; and generating a sequence of UUIDs. For example, to
69 | make a server info request using the client instance created above:
70 |
71 | ```Scala
72 | couch.server.info.run
73 | ```
74 |
75 | The `couch.server` property refers to an instance of the `Server` class, which
76 | represents the server API section. Then, calling the `info` method generates a
77 | [scalaz.concurrent.Task](https://github.com/scalaz/scalaz/blob/scalaz-seven/concurrent/src/main/scala/scalaz/concurrent/Task.scala),
78 | which describes an action of making a GET request to the server. At this point,
79 | an actual request is not yet made. Instead, `Task` encapsulates a description of
80 | a computation, which can be executed later. This allows us to control
81 | side-effects and keep the functions pure. Tim Perrett has written a very nice
82 | [blog
83 | post](http://timperrett.com/2014/07/20/scalaz-task-the-missing-documentation/)
84 | with more background and documentation on Scalaz's `Task`.
85 |
86 | The return type of `couch.server.info` is `Task[Res.ServerInfo]`, which means
87 | that when this task is executed, it may return a `ServerInfo` object or fail. To
88 | execute a `Task`, we need to call the `run` method, which triggers the actual
89 | GET request to server, whose response is then automatically parsed and mapped
90 | onto the `ServerInfo` case class that contains a few fields describing the
91 | server instance like the CouchDB version, etc. Ideally, instead of executing a
92 | `Task` and causing side-effects in the middle of a program, we should delay the
93 | execution as much as possible to keep the core application logic pure. Rather
94 | then executing `Task`s to obtain the query result, we can perform action on the
95 | query results and compose `Task`s in a functional way using higher-order
96 | functions like `map` and `flatMap`, or for-comprehensions. We will see more
97 | examples of this later. In further code snippets, I will omit calls to the `run`
98 | method assuming that the point where effectful computations are executed is
99 | externalized.
100 |
101 | The other operations of the server API can be performed in a similar way. To
102 | generate a UUID, you just need to call `couch.server.mkUuid`, which returns
103 | `Task[String]`. To generate `n` UUIDs, call `couch.server.mkUuids(n)`, which
104 | returns `Task[Seq[String]]` representing a task of generating a sequence of `n`
105 | UUIDs. For more usage examples, please refer to
106 | [ServerSpec](src/test/scala/com/ibm/couchdb/api/ServerSpec.scala).
107 |
108 |
109 | ### Databases API
110 |
111 | The databases API implements more useful functionality like creating, deleting,
112 | and getting information about databases. To create a database:
113 |
114 | ```Scala
115 | couch.dbs.create("awesome-database")
116 | ```
117 |
118 | The `couch.dbs` property refers to an instance of the `Databases` class, which
119 | represents the databases API section. A call to the `create` method returns a
120 | `Task[Res.Ok]`, which represents a request returning an instance of the `Res.Ok`
121 | case class if it succeeds, or a failure object if it fails. Failure handling is
122 | done using methods on `Task`, part of which are covered in Tim Perrett's [blog
123 | post](http://timperrett.com/2014/07/20/scalaz-task-the-missing-documentation/).
124 | In two words the actual result of a `Task` execution is `Throwable \/ A`, which
125 | is
126 | [either](https://github.com/scalaz/scalaz/blob/scalaz-seven/core/src/main/scala/scalaz/Either.scala)
127 | an exception or the desired type `A`. In the case or `dbs.create`, the desired
128 | result is of type `Res.Ok`, which is a case class representing a response from
129 | the server in case of a succeeded request.
130 |
131 | Other methods provided by the databases API are `dbs.delete("awesome-database")`
132 | to delete a database, `dbs.get("awesome-database")` to get information about a
133 | database returned as an instance of `DbInfo` case class that includes such
134 | fields as data size, number of documents in the database, etc. For some examples
135 | of using the databases API, please refer to
136 | [DatabasesSpec](src/test/scala/com/ibm/couchdb/api/DatabasesSpec.scala).
137 |
138 |
139 | ### Design API
140 |
141 | While the API sections described earlier operate at the level above databases,
142 | the Design, Documents, and Query APIs are applied within the context of a single
143 | database. Therefore, to obtain instances of these interfaces, the context needs
144 | to be specialized by specifying the name of a database of interest:
145 |
146 | ```Scala
147 | val db = couch.db("awesome-database", TypeMapping.empty)
148 | ```
149 |
150 | This method call returns an instance of the `CouchDbApi` case class representing
151 | the context of a single database, through which we can get access to the Design,
152 | Documents, and Query APIs. The `db` method takes 2 arguments: the database name
153 | and an instance of `TypeMapping`. We will discuss `TypeMapping` later, for now
154 | we can just pass an empty mapping using `TypeMapping.empty`. Through
155 | `CouchDbApi` we can obtain an instance of the `Design` class representing the
156 | Design API section for our database:
157 |
158 | ```Scala
159 | db.design
160 | ```
161 |
162 | The Design API allows us to create, retrieve, update, delete, and manage
163 | attachments to design documents stored in the current database (you can get the
164 | name of the database from an instance of `CouchDbApi` using `db.name`).
165 |
166 | Let's take a look at an example of a design document with a single view. First,
167 | assume we have a collection of people each corresponding to an object of a case
168 | class `Person` with a name and age fields:
169 |
170 | ```Scala
171 | case class Person(name: String, age: Int)
172 | ```
173 |
174 | Let's define a view with just a map function that emits person names as keys and
175 | ages as values. To do that, we are going to use a `CouchView` case class:
176 |
177 | ```Scala
178 | val ageView = CouchView(map =
179 | """
180 | |function(doc) {
181 | | emit(doc.doc.name, doc.doc.age);
182 | |}
183 | """.stripMargin)
184 | ```
185 |
186 | Basically, we define our map function in plain JavaScript and assign it to the
187 | `map` field of a `CouchView` object. This function maps each document to a pair
188 | of the person's name as the key and age as the value. Notice, that we need to
189 | use `doc.doc` to get to the fields of the person object for reasons that will
190 | become clear later.
191 | To define a view that contains a reduce operation, specify the relevant Javascript
192 | function to the `reduce` attribute of the `CouchView` case class constructor like so:
193 |
194 | ```Scala
195 | val totalAgeView = CouchView(map =
196 | """
197 | |function(doc) {
198 | | emit(doc._id, doc.doc.age);
199 | |}
200 | """.stripMargin,
201 | reduce =
202 | """
203 | |function(key, values, rereduce) {
204 | | return sum(values);
205 | |}
206 | """.stripMargin)
207 | ```
208 |
209 | We can now create an instance of our design document using
210 | the defined `ageView` and `totalAgeView`:
211 |
212 | ```Scala
213 | val designDoc = CouchDesign(
214 | name = "test-design",
215 | views = Map("age-view" -> ageView, "total-age-view" -> totalAgeView))
216 | ```
217 |
218 | `CouchDesign` supports other fields like `shows` and `lists`, but for this
219 | simple example we only specify the design `name` and `views` as a `Map` from
220 | view names to `CouchView` objects. Proper management of complex design documents
221 | is a separate topic (e.g., JavaScript functions can be stored in separate `.js`
222 | files and loaded dynamically). We can finally proceed to submitting the defined
223 | design document to our database:
224 |
225 | ```Scala
226 | db.design.create(designDoc)
227 | ```
228 |
229 | This method call returns an object of type `Task[Res.DocOk]`. The `DocOk` case
230 | class represents a response from the server to a succeeded request involving
231 | creating, modifying, and deleting documents. Compared with `Res.Ok`, it includes
232 | 2 extra fields: `id` (the ID of the created/updated/deleted document) and `rev`
233 | (the revision of the created/updated/deleted document). In the case of design
234 | documents, the ID is composed of the design name prefixed with `_design/`. In
235 | other words, `designDoc` will get the `_design/test-design` ID. Each revision is
236 | a unique 32-character UUID string. We can now retrieve the design document from
237 | the database by name or by ID:
238 |
239 | ```Scala
240 | db.design.get("test-design")
241 | db.design.getById("_design/test-design")
242 | ```
243 |
244 | Once the returned `Task`s are executed, each of these calls returns an instance
245 | of `CouchDesign` corresponding to our design document with some extra fields,
246 | e.g., `_id`, `_rev`, `_attachments`, etc. To update a design document, we must
247 | first retrieve it from the database to know the current revision and avoid
248 | [conflicts](http://guide.couchdb.org/draft/conflicts.html), make changes to the
249 | content, and submit the updated version. Let's say we want to add another view,
250 | which emits ages as keys and names as values assigned to a `nameView` variable,
251 | then our updated view `Map` is:
252 |
253 | ```Scala
254 | val updatedViews = Map(
255 | "age-view" -> ageView,
256 | "name-view" -> nameView)
257 | ```
258 |
259 | We can now submit the changes to the database as follows:
260 |
261 | ```Scala
262 | for {
263 | initial <- db.design.get("test-design")
264 | docOk <- db.design.update(initial.copy(views = updatedViews))
265 | } yield docOk
266 | ```
267 |
268 | Here, we use a for-comprehension to chain 2 monadic actions. If both actions
269 | succeed, we get a `Res.DocOk` object as a result containing the new revision of
270 | the design document stored in the `_rev` field. The Design API supports a few
271 | other operations, to see their usage examples please refer to
272 | [DesignSpec](src/test/scala/com/ibm/couchdb/api/DesignSpec.scala).
273 |
274 |
275 | ### Documents API
276 |
277 | The Documents API implements operations for creating, querying, modifying, and
278 | deleting documents and their attachments. At this stage, it's time to discuss
279 | how Scala objects are represented in CouchDB and what `TypeMapping` is used for.
280 | One of the design goals of `CouchDB-Scala` is to make it as easy as possible to
281 | store and retrieve documents by automating the process of serialization and
282 | deserialization to and from JSON. This functionality is based on
283 | [uPickle](https://github.com/lihaoyi/upickle-pprint), which uses macros to
284 | automatically generate readers and writers for case classes. However, it also
285 | allows implementing custom readers and writers for your domain classes if they
286 | are not *case classes*. For example, these can be
287 | [Thrift](https://thrift.apache.org/) /
288 | [Scrooge](https://github.com/twitter/scrooge) generated entities or your custom
289 | classes.
290 |
291 | CouchDB automatically adds several fields to every document containing metadata
292 | about the document, such as `_id`, `_rev`, `_attachments`, `_conflicts`, etc. To
293 | take advantage of uPickle's support for case classes, a decision was made to
294 | have a case class called `CouchDoc[D]` that has all the metadata fields
295 | generated by CouchDB and also includes 2 special fields: `doc` for storing an
296 | instance of your domain class `D`, and `kind` for storing a string
297 | representation of the document type that can be used for filtering in views,
298 | shows, and lists (we use `kind` instead of `type` here, as `type` is a reserved
299 | keyword in Scala). In other words, if your domain model is represented by a set
300 | of case classes, the serialization and deserialization will be handled
301 | completely transparently for you. `TypeMapping` is used for defining a mapping
302 | from you domain model classes to a string representation of the corresponding
303 | document type. Continuing the previous example with the `Person` case class, we
304 | can define a `TypeMapping`, for example, as follows:
305 |
306 | ```Scala
307 | val typeMapping = TypeMapping(classOf[Person] -> "Person")
308 | ```
309 |
310 | Here, we are specifying a mapping from the class name `Person` to a document
311 | kind as a string. The `TypeMapping` factory maps classes to their canonical
312 | names to preserve uniqueness. Whenever a document is submitted to the database,
313 | the `kind` field is automatically populated based on the specified mapping. If
314 | the type mapping is not specified (as we did above by using
315 | `TypeMapping.empty`), the `kind` field is ignored. We can now provide the newly
316 | defined `TypeMapping` to create a fully specified database context:
317 |
318 | ```Scala
319 | val db = couch.db("awesome-database", typeMapping)
320 | ```
321 |
322 | Similarly to the other API sections, we can use the database context to get an
323 | instance of the `Documents` class representing the Documents API section:
324 |
325 | ```Scala
326 | db.docs
327 | ```
328 |
329 | Let's define some data:
330 |
331 | ```Scala
332 | val alice = Person("Alice", 25)
333 | val bob = Person("Bob", 30)
334 | val carl = Person("Carl", 20)
335 | ```
336 |
337 | We can now store these objects in the database as follows:
338 |
339 | ```Scala
340 | db.docs.create(alice)
341 | ```
342 |
343 | This method assigns a UUID generated with `server.mkUuid` that we've seen above
344 | to the document being stored. Another option is to specify our own document ID
345 | if it's known to be unique:
346 |
347 | ```Scala
348 | db.docs.create(bob, "bob")
349 | ```
350 |
351 | As another alternative, we can create multiple documents with auto-generated
352 | UUIDs at once using a batch request:
353 |
354 | ```Scala
355 | db.docs.createMany(Seq(alice, bob, carl))
356 | ```
357 |
358 | We can retrieve a document from the database by ID:
359 |
360 | ```Scala
361 | db.docs.get[Person]("bob")
362 | ```
363 |
364 | Here, we have to be explicit about the expected object type to allow uPickle to
365 | do its magic, that's why we specify the type parameter to the `get` method. This
366 | method returns `Task[CouchDoc[Person]]`, which basically means that we are
367 | getting back a task that after executing successfully will give us an instance
368 | of `CouchDoc[Person]`. This object will contain an instance of `Person` in the
369 | `doc` field equivalent to the original `Person("Bob", 30)`.
370 |
371 | You can also retrieve a set of documents by IDs using:
372 |
373 | ```Scala
374 | db.docs.getMany.includeDocs[Person].withIds(Seq("id1", "id1")).build.query
375 | ```
376 |
377 | A call to `getMany` returns an instance of `GetManyDocumentsQueryBuilder`, which
378 | is a class allowing you to build a query in a type-safe way. Under the hood, it
379 | makes a request to the
380 | [/{db}/_all_docs](http://docs.couchdb.org/en/1.6.1/api/database/bulk-api.html#get--db-_all_docs)
381 | endpoint. As you can see from the linked documentation on this endpoint, it has
382 | many optional parameters. The `GetManyDocumentsQueryBuilder` class provides a
383 | fluent interface for constructing queries to this endpoint. For example, to
384 | limit the number of documents to the maximum of 10 and return them in the
385 | descending order:
386 |
387 | ```Scala
388 | db.docs.getMany.limit(10).descending.includeDocs[Person].withIds(Seq("id1", "id2")).build.query
389 | ```
390 |
391 | This creates an instance of `Task[CouchDocs[String, CouchDocRev, Person]]`,
392 | which looks complicated but just represents a task that returns basically a
393 | sequence of documents. The `queryIncludeDocs` method serves as a way to complete
394 | the query construction process, which also sets the `include_docs` option to
395 | include the full content of the documents mapped to `Person` objects on arrival.
396 |
397 | It's also possible to execute a query without including the document content
398 | using `db.docs.getMany.build.query`, which is equivalent to keeping the `include_docs`
399 | set to its default `false` value. This query will only return metadata on the
400 | matching documents. In this case, we don't need to specify the type parameter as
401 | no mapping is required since the document content is not retrieved.
402 |
403 | To retrieve all documents in the database of a given type without specifying ids, you could use
404 | one of the following approaches:
405 | ```Scala
406 | val allPeople1 = db.docs.getMany.byTypeUsingTemporaryView[Person].build.query
407 | ```
408 |
409 | The first approach, `byTypeUsingTemporaryView[T]`, uses a temporary view
410 | under the hood for type based filtering. While convenient for development purposes, it is inefficient
411 | and should not be used in production.
412 |
413 | For efficiency you should instead use `byType[K, V](view_name)`, or the simpler
414 | `byType[V](view_name)`, which require that you first create
415 | a type filtering permanent view, and then pass its name as argument to one of these methods.
416 | Because a permanent view is used, these approaches are more efficient and are thus the recommended
417 | approach for type based document filtering.
418 |
419 | Note in `byType[K, V](view_name)` the parameters `K` and `V` represent the key and value types
420 | of the permanent view. The document's `kind` attribute must be the first key of such a
421 | view, as in the type filter view function example shown below.
422 |
423 | ```javascript
424 | function(doc) {
425 | emit([doc.kind, doc._id], doc._id);
426 | }
427 | ```
428 |
429 | The above function could then be used as follows:
430 | ```scala
431 | val allPeople2 = db.docs.getMany.byType[(String, String), String](your_view_name).build.query
432 | ```
433 |
434 | In the simpler `byType[V](view_name)`, `K` is implicitly assumed to be of type Tuple of two strings
435 | `(String, String)`. Note the document's `kind` attribute must be the first key of such a
436 | view, as in the type filter view function example defined above.
437 |
438 | The above function could then be used as follows:
439 | ```scala
440 | val allPeople3 = db.docs.getMany.byType[String](your_view_name).build.query
441 | ```
442 |
443 | There is a similar query builder for retrieving single documents
444 | `GetDocumentQueryBuilder` that makes GET requests to the
445 | [/{db}/{docid}](http://docs.couchdb.org/en/1.6.1/api/document/common.html#get--db-docid)
446 | endpoint. This query builder can accessed through `db.docs.get`.
447 |
448 | There are other operations provided by the Documents API, such as updating
449 | documents, deleting documents, adding attachments, retrieving attachments, etc.
450 | For more usage examples, please refer to
451 | [DocumentsSpec](src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala).
452 |
453 |
454 | ### Query API
455 |
456 | The Query API provides an interface for querying views, shows, and lists. Let's
457 | say we want to query our `age-view` defined earlier. To do that, we first
458 | obtain an instance of `ViewQueryBuilder` as follows:
459 |
460 | ```Scala
461 | val ageView = db.query.view[String, Int]("test-design", "age-view").get
462 | val totalAgeView = db.query.view[String, Int]("test-design", "total-age-view").get
463 | ```
464 |
465 | We need to specify 2 type parameters to the `view` method representing the types
466 | of the key and value emitted by the view. In the case of `age-view` and `total-age-view`, it's
467 | `String` for the key (person name) and `Int` for the value (person age).
468 |
469 | We can now use the `ageView` query builder to retrieve all the documents from the view:
470 |
471 | ```Scala
472 | ageView.build.query
473 | ```
474 |
475 | This method call returns an instance of `Task[CouchKeyVals[String, Int]]`.
476 | Since we haven't specified the `include_docs` option, this query only retrieves
477 | a sequence of document IDs, keys, and values emitted by the view's map function.
478 | This method makes a call to the
479 | [/{db}/_design/{ddoc}/_view/{view}](http://docs.couchdb.org/en/1.6.1/api/ddoc/views.html#get--db-_design-ddoc-_view-view)
480 | endpoint, and the builder supports all the relevant options.
481 |
482 | Similarly, to query the total age of Persons in the document using the
483 | `totalAgeView` builder we can do:
484 |
485 | ```Scala
486 | totalAgeView.reduce[Int].build.query
487 | ```
488 |
489 | The type parameter `T` specified to `queryWithReduce[T]`, in this case `Int`,
490 | is the expected return type of the view's `reduce` function.
491 |
492 | We can also make more complex queries. Let's say we want to get 10 people
493 | starting from the name Bob and include the document content:
494 |
495 | ```Scala
496 | ageView.startKey("Bob").limit(10).includeDocs[Person].build.query
497 | ```
498 |
499 | This returns an instance of `Task[CouchDocs[String, Int, Person]]`, which once
500 | executed results in a sequence of objects encapsulating the metadata about the
501 | documents (`id`, `key`, `value`, `offset`, `total_rows`) and the corresponding
502 | `Person` objects. Please follow the definitions of case classes in
503 | [Model](src/main/scala/com/ibm/couchdb/Model.scala)
504 | to fully understand the structure of the returned objects.
505 |
506 | It's also possible to only get the documents from a view that match the
507 | specified keys. For example, we can use that to get only documents of Alice and
508 | Carl:
509 |
510 | ```Scala
511 | ageView.withIds(Seq("Alice", "Carl")).build.query
512 | ```
513 |
514 | This return an instance of `Task[CouchKeyVals[String, Int]]`. For other usage
515 | examples of the view Query API, please refer to
516 | [QueryViewSpec](src/test/scala/com/ibm/couchdb/api/QueryViewSpec.scala).
517 |
518 | The APIs for querying shows and lists are structured similarly to view querying
519 | and follow the official CouchDB specification. Please refer to
520 | [QueryShowSpec](src/test/scala/com/ibm/couchdb/api/QueryShowSpec.scala)
521 | and
522 | [QueryListSpec](src/test/scala/com/ibm/couchdb/api/QueryListSpec.scala)
523 | for more details and examples.
524 |
525 |
526 | ### Authentication
527 |
528 | At the moment, the client supports only the [basic
529 | authentication](http://docs.couchdb.org/en/1.6.1/api/server/authn.html#basic-authentication)
530 | method. To use it, just pass your username and password to the `CouchDb`
531 | factory:
532 |
533 | ```Scala
534 | val couch = CouchDb("127.0.0.1", 6984, https = true, "username", "password")
535 | ```
536 |
537 | Please note that [enabling
538 | HTTPS](http://docs.couchdb.org/en/1.6.1/config/http.html#config-ssl) is
539 | recommended to avoid sending your credentials in plain text. The default CouchDB
540 | HTTPS port is 6984.
541 |
542 |
543 | ### Complete example
544 |
545 | Here is a basic example of an application that stores a set of case class
546 | instances in a database, retrieves them back, and prints out afterwards:
547 |
548 | ```Scala
549 | object Basic extends App {
550 |
551 | // Define a simple case class to represent our data model
552 | case class Person(name: String, age: Int)
553 |
554 | // Define a type mapping used to transform class names into the doc kind
555 | val typeMapping = TypeMapping(classOf[Person] -> "Person")
556 |
557 | // Define some sample data
558 | val alice = Person("Alice", 25)
559 | val bob = Person("Bob", 30)
560 | val carl = Person("Carl", 20)
561 |
562 | // Create a CouchDB client instance
563 | val couch = CouchDb("127.0.0.1", 5984)
564 | // Define a database name
565 | val dbName = "couchdb-scala-basic-example"
566 | // Get an instance of the DB API by name and type mapping
567 | val db = couch.db(dbName, typeMapping)
568 |
569 | val actions = for {
570 | // Delete the database or ignore the error if it doesn't exist
571 | _ <- couch.dbs.delete(dbName).ignoreError
572 | // Create a new database
573 | _ <- couch.dbs.create(dbName)
574 | // Insert documents into the database
575 | _ <- db.docs.createMany(Seq(alice, bob, carl))
576 | // Retrieve all documents from the database and unserialize to Person
577 | docs <- db.docs.getMany.includeDocs[Person].build.query
578 | } yield docs.getDocsData
579 |
580 | // Execute the actions and process the result
581 | actions.attemptRun match {
582 | // In case of an error (left side of Either), print it
583 | case -\/(e) => println(e)
584 | // In case of a success (right side of Either), print each object
585 | case \/-(a) => a.map(println(_))
586 | }
587 |
588 | }
589 | ```
590 |
591 | You can run this example from the project directory using `sbt`:
592 |
593 | ```Bash
594 | sbt "run-main com.ibm.couchdb.examples.Basic"
595 | ```
596 |
597 |
598 | ## Mailing list
599 |
600 | Please feel free to join our mailing list, we welcome all questions and
601 | suggestions: https://groups.google.com/forum/#!forum/couchdb-scala
602 |
603 |
604 | ## Contributing
605 |
606 | We welcome contributions, but request you follow these guidelines. Please raise
607 | any bug reports on the project's [issue
608 | tracker](https://github.com/beloglazov/couchdb-scala/issues).
609 |
610 | In order for us to accept pull-requests, the contributor must first complete a
611 | Contributor License Agreement (CLA). This clarifies the intellectual property
612 | license granted with any contribution. It is for your protection as a
613 | Contributor as well as the protection of IBM and its customers; it does not
614 | change your rights to use your own Contributions for any other purpose.
615 |
616 | You can download the CLAs here:
617 |
618 | - [individual](cla/cla-individual.pdf)
619 | - [corporate](cla/cla-corporate.pdf)
620 |
621 | If you are an IBMer, please contact us directly as the contribution process is
622 | slightly different.
623 |
624 |
625 | ## Contributors
626 |
627 | - [Anton Beloglazov](http://beloglazov.info/) ([@beloglazov](https://github.com/beloglazov))
628 | - Ermyas Abebe ([@ermyas](https://github.com/ermyas))
629 |
630 |
631 | ## Copyright and license
632 |
633 | © Copyright 2015 IBM Corporation, Google Inc. Distributed under the [Apache 2.0
634 | license](LICENSE).
635 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | import xerial.sbt.Sonatype.SonatypeKeys._
2 |
3 | sonatypeSettings
4 |
5 | profileName := "com.ibm.couchdb-scala"
6 |
7 | organization := "com.ibm"
8 |
9 | name := "couchdb-scala"
10 |
11 | version := "0.8.0-SNAPSHOT"
12 |
13 | scalaVersion := "2.11.8"
14 |
15 | description := "A purely functional Scala client for CouchDB"
16 |
17 | homepage := Some(url("https://github.com/beloglazov/couchdb-scala"))
18 |
19 | licenses := Seq("The Apache Software License, Version 2.0"
20 | -> url("http://www.apache.org/licenses/LICENSE-2.0.txt"))
21 |
22 | libraryDependencies ++= Seq(
23 | "org.scalaz" %% "scalaz-core" % "7.2.6",
24 | "org.scalaz" %% "scalaz-effect" % "7.2.6",
25 | "org.http4s" %% "http4s-core" % "0.14.6a",
26 | "org.http4s" %% "http4s-client" % "0.14.6a",
27 | "org.http4s" %% "http4s-blaze-client" % "0.14.6a",
28 | "com.lihaoyi" %% "upickle" % "0.4.1",
29 | "com.github.julien-truffaut" %% "monocle-core" % "1.2.2",
30 | "com.github.julien-truffaut" %% "monocle-macro" % "1.2.2",
31 | "org.log4s" %% "log4s" % "1.3.0",
32 | "ch.qos.logback" % "logback-classic" % "1.1.7",
33 | "org.specs2" %% "specs2" % "3.7" % "test",
34 | "org.typelevel" %% "scalaz-specs2" % "0.3.0" % "test",
35 | "org.scalacheck" %% "scalacheck" % "1.13.0" % "test",
36 | "org.scalaz" %% "scalaz-scalacheck-binding" % "7.2.1" % "test"
37 | )
38 |
39 | scalacOptions ++= Seq(
40 | "-deprecation",
41 | "-encoding", "UTF-8",
42 | "-feature",
43 | "-language:existentials",
44 | "-language:higherKinds",
45 | "-language:implicitConversions",
46 | "-language:postfixOps",
47 | "-unchecked",
48 | "-Xfatal-warnings",
49 | "-Xlint",
50 | "-Yno-adapted-args",
51 | "-Ywarn-numeric-widen",
52 | "-Ywarn-value-discard",
53 | "-Xfuture",
54 | "-Ywarn-unused-import"
55 | )
56 |
57 | scalacOptions in (Compile, console) ~= (_ filterNot (
58 | List("-Ywarn-unused-import", "-Xfatal-warnings").contains(_)))
59 |
60 | wartremover.wartremoverSettings
61 |
62 | wartremover.wartremoverErrors in (Compile, compile) ++= Seq(
63 | wartremover.Wart.Any,
64 | wartremover.Wart.Any2StringAdd,
65 | wartremover.Wart.EitherProjectionPartial,
66 | wartremover.Wart.OptionPartial,
67 | wartremover.Wart.Product,
68 | wartremover.Wart.Serializable,
69 | wartremover.Wart.ListOps
70 | )
71 |
72 | lazy val compileScalastyle = taskKey[Unit]("compileScalastyle")
73 |
74 | compileScalastyle := org.scalastyle.sbt.ScalastylePlugin.scalastyle.in(Compile).toTask("").value
75 |
76 | (compile in Compile) <<= (compile in Compile) dependsOn compileScalastyle
77 |
78 | lazy val testScalastyle = taskKey[Unit]("testScalastyle")
79 |
80 | testScalastyle := org.scalastyle.sbt.ScalastylePlugin.scalastyle.in(Test).toTask("").value
81 |
82 | (test in Test) <<= (test in Test) dependsOn testScalastyle
83 |
84 | testFrameworks := Seq(TestFrameworks.Specs2, TestFrameworks.ScalaCheck)
85 |
86 | parallelExecution in Test := false
87 |
88 | unmanagedSourceDirectories in Compile += baseDirectory.value / "examples" / "src" / "main" / "scala"
89 |
90 | initialCommands in console := "import scalaz._, Scalaz._, com.ibm.couchdb._"
91 |
92 | initialCommands in console in Test := "import scalaz._, Scalaz._, scalacheck.ScalazProperties._, " +
93 | "scalacheck.ScalazArbitrary._,scalacheck.ScalaCheckBinding._"
94 |
95 | logBuffered := false
96 |
97 | publishMavenStyle := true
98 |
99 | publishArtifact in Test := false
100 |
101 | pomExtra := {
102 |
103 | scm:git:git@github.com:beloglazov/couchdb-scala.git
104 | scm:git:git@github.com:beloglazov/couchdb-scala.git
105 | https://github.com/beloglazov/couchdb-scala
106 |
107 |
108 |
109 | beloglazov
110 | Anton Beloglazov
111 | anton.beloglazov@gmail.com
112 | http://beloglazov.info
113 |
114 |
115 | }
116 |
--------------------------------------------------------------------------------
/cla/cla-corporate.doc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beloglazov/couchdb-scala/b97f173c5f2803d18d0536f39e8063e4abe28f6b/cla/cla-corporate.doc
--------------------------------------------------------------------------------
/cla/cla-corporate.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beloglazov/couchdb-scala/b97f173c5f2803d18d0536f39e8063e4abe28f6b/cla/cla-corporate.pdf
--------------------------------------------------------------------------------
/cla/cla-individual.doc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beloglazov/couchdb-scala/b97f173c5f2803d18d0536f39e8063e4abe28f6b/cla/cla-individual.doc
--------------------------------------------------------------------------------
/cla/cla-individual.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/beloglazov/couchdb-scala/b97f173c5f2803d18d0536f39e8063e4abe28f6b/cla/cla-individual.pdf
--------------------------------------------------------------------------------
/cloudant-supported-features.md:
--------------------------------------------------------------------------------
1 | IBM Cloudant API V2 Support
2 |
3 | Endpoints are prefixed with `/{username}.cloudant.com/`
4 |
5 | | Cloudant feature | HTTP API | Support | Since | Example |
6 | |---|---|:---:|:---:|:----:|
7 | |**Databases**|||||
8 | | Create a new database |`PUT /{db}` | | | |
9 | | Get information about a database | `GET /{db}` | | | |
10 | | Delete a specified database |`DELETE /{db}` | | | |
11 | | List databases | `GET /_all_dbs ` | | | |
12 | | Get all documents in a database |`GET /{db}/_all_docs` | | | |
13 | | Create a new index | `POST /{db}/_index` | | | |
14 | | Delete an index | `DELETE /{db}/_index/{design_doc}/{type}/{name}` | | | |
15 | |**Documents**|||||
16 | | Create document | `POST /{db}/` | | | |
17 | | Retrieve document | `GET /{db}/{doc_id}` | | | |
18 | | Update document | `PUT /{db}/{doc_id}` | | | |
19 | | Query database | `GET /{db}/_all_docs` | | | |
20 | | Delete document | `DELETE /{db}/{doc_id}?rev={rev}` | | | |
21 | | Create and update multiple documents | `POST /{db}/_bulk_docs` | | | |
22 | | Create attachment | `PUT /{db}/{doc_id}/{att_name}?rev={rev}` | | | |
23 | | Retrieve attachment | `GET /{db}/{doc_id}/{att_name}` | | | |
24 | | Delete attachment | `DELETE /{db}/{doc_id}/{attachment_name}?rev={rev}` | | | |
25 | | Find document using an index | `POST /{db}/_find` | | | |
26 | | Get documents given multiple keys | `POST /{db}/_design/{design_doc}/_view` | | | |
27 | | Get list of changes to documents | `GET /{db}/_changes` | | | |
28 | |**Design Documents**|||||
29 | | Create design document | `PUT /{db}/_design/design-doc` | | | |
30 | | Update design document | `PUT /{db}/_design/design-doc` | | | |
31 | | Get design document | `GET /{db}/_design/{des_doc}` | | | |
32 | | Get meta-data about design document | `GET /{db}/_design/{des_doc}/_info` | | | |
33 | | Copy design document | `COPY /{db}/_design/{des_doc}?rev={rev}` | | | |
34 | | Delete design document | `DELETE /{db}/_design/{des_doc}?rev={rev}` | | | |
35 | | Get List Functions | `GET /{db}/{design_id}/_list/{list_function}/{map_reduce_index}` | | | |
36 | | Get Show Functions | `GET /{db}/{design_id}/_show/{show_function}/{document_id}` | | | |
37 | | Query Update Handlers | `POST /{db}/{design_id}/_update/{update_handler}` | | | |
38 | |**Views**|||||
39 | | Add view to design document | `PUT /{db}/_design/` | | | |
40 | | Query a view| `GET /{db}/_design/{design_id}/_view` | | | |
41 | | Query a view using a list of keys| `POST /{db}/_design/{design_id}/_view/{view_name}` | | | |
42 | |**Replication**|||||
43 | | | | | | |
44 | |**Server**|||||
45 | | List active tasks | `GET /_active_tasks` | | | |
46 | | List database events | `GET /_db_updates` | | | |
47 | | Request a Universally Unique Identifier | `GET /_uuid` | | | |
48 |
--------------------------------------------------------------------------------
/code-style/intellij-code-style.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/couchdb-supported-features.md:
--------------------------------------------------------------------------------
1 | Apache Couch Db version **1.6**
2 |
3 | | CouchDb feature | HTTP API | Support | Since | Example |
4 | |---|---|:---:|:---:|:----:|
5 | |**Databases**|||||
6 | | Get information about a database | `/{db}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DatabasesSpec.scala#L36-43)|
7 | | Create a new database | `/{db}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DatabasesSpec.scala#L30-34)|
8 | | Delete a specified database | `/{db}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DatabasesSpec.scala#L51-56)|
9 | | Batch mode writes | `/{db}?batch=ok` | | | |
10 | |**Documents**|||||
11 | | Retrieve document | `/{db}/{doc_id}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L45-54)|
12 | | Retrieve document by revision number | `/{db}/{doc_id}` | | | |
13 | | Create document | `/{db}/` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L40-43)|
14 | | Get list of document revisions | `/{db}/{doc_id}` | | | |
15 | | Update document | `/{db}/{doc_id}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L122-132)|
16 | | Delete document | `/{db}/{doc_id}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/cohdb/api/DocumentsSpec.scala#L149-154)|
17 | | Copy document | `/{db}/{doc_id}` | | | |
18 | | Copy document by revision | `/{db}/{doc_id}` | | | |
19 | | Copy to an existing document | `/{db}/{doc_id}` | | | |
20 | | Get attachment information | `/{db}/{doc_id}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L172-188)|
21 | | Create single attachment | `/{db}/{doc_id}/{att_name}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L156-160)|
22 | | Create multiple attachment | `/{db}/{doc_id}` | | | |
23 | | Retrieve attachment | `/{db}/{doc_id}/{att_name}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L162-170)|
24 | | Retrieve multiple attachments | `/{db}/{doc_id}` | | | |
25 | | Delete attachment | `/{db}/{doc_id}/{attachment_name}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L209-218)|
26 | | Get all documents in a database | ` /{db}/_all_docs` | ✓ | 0.5 | [view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L70-79)|
27 | | Get documents given multiple keys | `/{db}/_all_docs` | ✓ | 0.5 | [view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L81-91)|
28 | | Create and update multiple documents | `/{db}/_bulk_docs` | ✓ | 0.5 | [view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala#L62-68)|
29 | | Get list of changes to documents | `/{db}/_changes` | | | |
30 | | Get list of changes for specified document ids | `/{db}/_changes` | | | |
31 | | Compact database | `/{db}/_compact` | | | |
32 | | Compact view indexes associated with specified design document | `/{db}/_compact/{des_doc}` | | | |
33 | | Commit recent changes to disk | `/{db}/_ensure_full_commit` | | | |
34 | | Remove view index files that are not required | `/{db}/_view_cleanup` | | | |
35 | | Get security object from database | `/{db}/_security` | | | |
36 | | Create and execute temporary view | `/{db}/_temp_view` | | | |
37 | | Permanently remove references to delete documents | `/{db}/_purge` | | | |
38 | | Given list of document revisions, return this that do not exist| `/{db}/_missing_revs` | | | |
39 | | Given list of revision ids, returns those that do not correspond to revisions in database | `/{db}/_revs_diff` | | | |
40 | | Get the current revision limit setting | `/{db}/_revs_limit` | | | |
41 | |**Design Documents**|||||
42 | | Get content of design document | `/{db}/_design/{des_doc}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DesignSpec.scala#L46-53)|
43 | | Get meta-data about design document | `/{db}/_design/{des_doc}/_info` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DesignSpec.scala#L37-44)|
44 | | Create design document | `/{db}/_design/{des_doc}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DesignSpec.scala#L32-35)|
45 | | Update design document | `/{db}/_design/{des_doc}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DesignSpec.scala#L64-75)|
46 | | Delete design document | `/{db}/_design/{des_doc}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DesignSpec.scala#L77-82)|
47 | | Copy design document | `/{db}/_design/{des_doc}` | | | |
48 | | Create attachment | `/{db}/_design/{des_doc}/{att_name}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DesignSpec.scala#L90-95)|
49 | | Get attachment | `/{db}/_design/{des_doc}/{att_name}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DesignSpec.scala#L97-103)|
50 | | Delete design document attachment | `/{db}/_design/{des_doc}/{att_name}` | ✓ | 0.5 |[view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DesignSpec.scala#L77-82)|
51 | | Execute specified view function from the specified design document | `/{db}/_design/{des_doc}/_view/{view_name}` | ✓ | 0.5 | [view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/QueryViewSpec.scala)|
52 | | Applies show function for specified documents| `/{db}/_design/{des_doc} /_show/{show_name}/{doc_id}` | ✓ | 0.5 | [view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/QueryShowSpec.scala)|
53 | | Applies list function for the view function from the same design document | `/{db}/_design/{des_doc} /_list/{list_name}/{view_name}` | ✓ | 0.5 | [view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/QueryListSpec.scala)|
54 | | Applies list function for the view function from another design document | `/{db}/_design/{des_doc} / _list/{list_name}/{other_design_doc}/{view_name}`| | | |
55 | | Rewrite the specified path by rules in specified design document | `/{db}/_design/{des-doc}/_rewrite/path` | | | |
56 | |**Local Documents**|||||
57 | | Get local document | `/{db}/_local/{doc_id}` | | | |
58 | | Store local document | `/{db}/_local/{doc_id}` | | | |
59 | | Delete local document | `/{db}/_local/{doc_id}` | | | |
60 | | Copy local document | `/{db}/_local/{doc_id}` | | | |
61 | |**Server**|||||
62 | | Get meta information about instance | `/` | ✓ | 0.5 | [view] (https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/ServerSpec.scala#L28-33) |
63 | | Request a Universally Unique Identifier | `/_uuid` | ✓ | 0.5 | [view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/ServerSpec.scala#L31-37) |
64 | | List databases | `/_all_dbs` | ✓ | 0.5 | [view](https://github.com/beloglazov/couchdb-scala/blob/5b2e78838c53d1e21a47e3ef8c42d0cc5bb1dcae/src/test/scala/com/ibm/couchdb/api/DatabasesSpec.scala#L45-49)|
65 | | List active tasks | `/_active_tasks` | | | |
66 | | List database events | `/_db_updates` | | | |
67 | | View Logs | `/_log` | | | |
68 | | Request, configure or stop a replication request | `/_replicate` | | | |
69 | | Restart instance | `/_restart` | | | |
70 | | Statistics about server | `/_stats` | | | |
71 |
--------------------------------------------------------------------------------
/examples/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/examples/src/main/scala/com/ibm/couchdb/examples/Basic.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.examples
18 |
19 | import com.ibm.couchdb._
20 | import org.slf4j.LoggerFactory
21 |
22 | import scalaz._
23 | import scalaz.concurrent.Task
24 |
25 | object Basic extends App {
26 | private val logger = LoggerFactory.getLogger(Basic.getClass)
27 |
28 | // Define a simple case class to represent our data model
29 | case class Person(name: String, age: Int)
30 |
31 | // Define a type mapping used to transform class names into the doc kind
32 | val typeMapping = TypeMapping(classOf[Person] -> "Person")
33 |
34 | // Define some sample data
35 | val alice = Person("Alice", 25)
36 | val bob = Person("Bob", 30)
37 | val carl = Person("Carl", 20)
38 |
39 | // Create a CouchDB client instance
40 | val couch = CouchDb("127.0.0.1", 5984)
41 | // Define a database name
42 | val dbName = "couchdb-scala-basic-example"
43 | // Get an instance of the DB API by name and type mapping
44 | val db = couch.db(dbName, typeMapping)
45 |
46 | typeMapping.get(classOf[Person]).foreach { mType =>
47 | val actions: Task[Seq[Person]] = for {
48 | // Delete the database or ignore the error if it doesn't exist
49 | _ <- couch.dbs.delete(dbName).ignoreError
50 | // Create a new database
51 | _ <- couch.dbs.create(dbName)
52 | // Insert documents into the database
53 | _ <- db.docs.createMany(Seq(alice, bob, carl))
54 | // Retrieve all documents from the database and unserialize to Person
55 | docs <- db.docs.getMany.includeDocs[Person].byTypeUsingTemporaryView(mType).build.query
56 | } yield docs.getDocsData
57 |
58 | // Execute the actions and process the result
59 | actions.unsafePerformSyncAttempt match {
60 | // In case of an error (left side of Either), print it
61 | case -\/(e) => logger.error(e.getMessage, e)
62 | // In case of a success (right side of Either), print each object
63 | case \/-(a) => a.foreach(x => logger.info(x.toString))
64 | }
65 | couch.client.client.shutdownNow()
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | resolvers += Resolver.sonatypeRepo("releases")
2 |
3 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.1.0-RC1")
4 |
5 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.2")
6 |
7 | addSbtPlugin("org.brianmckenna" % "sbt-wartremover" % "0.14")
8 |
9 | addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.8.0")
10 |
11 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0")
12 |
13 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "0.2.1")
--------------------------------------------------------------------------------
/scalastyle-config.xml:
--------------------------------------------------------------------------------
1 |
2 | Scalastyle standard configuration
3 |
4 |
5 |
6 |
7 |
8 |
9 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/CouchDb.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb
18 |
19 | import com.ibm.couchdb.api.{Databases, Design, Documents, Query, Server}
20 | import com.ibm.couchdb.core.Client
21 |
22 | import scalaz.Scalaz._
23 | import scalaz._
24 |
25 | case class CouchDbApi(name: String, docs: Documents, design: Design, query: Query)
26 |
27 | class CouchDb private(
28 | host: String,
29 | port: Int,
30 | https: Boolean,
31 | credentials: Option[(String, String)]) {
32 |
33 | val client = new Client(Config(host, port, https, credentials))
34 | val server = new Server(client)
35 | val dbs = new Databases(client)
36 |
37 | private val memo = Memo.mutableHashMapMemo[(String, TypeMapping), CouchDbApi] {
38 | case (db, types) =>
39 | CouchDbApi(
40 | db,
41 | new Documents(client, db, types),
42 | new Design(client, db),
43 | new Query(client, db))
44 | }
45 |
46 | def db(name: String, types: TypeMapping): CouchDbApi = memo((name, types))
47 |
48 | }
49 |
50 | object CouchDb {
51 |
52 | def apply(host: String, port: Int, https: Boolean = false): CouchDb = {
53 | new CouchDb(host, port, https, none)
54 | }
55 |
56 | def apply(
57 | host: String, port: Int, https: Boolean, username: String, password: String): CouchDb = {
58 | new CouchDb(host, port, https, (username, password).some)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/Lenses.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb
18 |
19 | import monocle.PLens
20 |
21 | object Lenses {
22 |
23 | def _couchDoc[A, B]: PLens[CouchDoc[A], CouchDoc[B], A, B] =
24 | PLens[CouchDoc[A], CouchDoc[B], A, B](_.doc)(d => cd => cd.copy(doc = d))
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/Model.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation, Google Inc.
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 |
17 | package com.ibm.couchdb
18 |
19 | import java.util.Base64
20 |
21 | import org.http4s.headers.`Content-Type`
22 |
23 | import scalaz.\/
24 |
25 | case class Config(host: String, port: Int, https: Boolean, credentials: Option[(String, String)])
26 |
27 | case class CouchDoc[D](
28 | doc: D,
29 | kind: String,
30 | _id: String = "",
31 | _rev: String = "",
32 | _deleted: Boolean = false,
33 | _attachments: Map[String, CouchAttachment] = Map.empty[String, CouchAttachment],
34 | _conflicts: Seq[String] = Seq.empty[String],
35 | _deleted_conflicts: Seq[String] = Seq.empty[String],
36 | _local_seq: Int = 0)
37 |
38 | case class CouchDocRev(rev: String)
39 |
40 | case class CouchKeyVal[K, V](id: String, key: K, value: V)
41 |
42 | case class CouchReducedKeyVal[K, V](key: K, value: V)
43 |
44 | case class CouchKeyError[K](key: K, error: String)
45 |
46 | case class CouchKeyValWithDoc[K, V, D](id: String, key: K, value: V, doc: CouchDoc[D])
47 |
48 | case class CouchKeyVals[K, V](offset: Int, total_rows: Int, rows: Seq[CouchKeyVal[K, V]])
49 |
50 | case class CouchReducedKeyVals[K, V](rows: Seq[CouchReducedKeyVal[K, V]])
51 |
52 | case class CouchKeyValsIncludesMissing[K, V](
53 | offset: Int,
54 | total_rows: Int,
55 | rows: Seq[\/[CouchKeyError[K], CouchKeyVal[K, V]]])
56 |
57 | case class CouchDocs[K, V, D](
58 | offset: Int, total_rows: Int, rows: Seq[CouchKeyValWithDoc[K, V, D]]) {
59 |
60 | def getDocs: Seq[CouchDoc[D]] = rows.map(_.doc)
61 |
62 | def getDocsData: Seq[D] = rows.map(_.doc.doc)
63 | }
64 |
65 | case class CouchDocsIncludesMissing[K, V, D](
66 | offset: Int,
67 | total_rows: Int,
68 | rows: Seq[\/[CouchKeyError[K], CouchKeyValWithDoc[K, V, D]]]) {
69 |
70 | def getDocs: Seq[CouchDoc[D]] = rows.flatMap(_.toOption).map(_.doc)
71 |
72 | def getDocsData: Seq[D] = rows.flatMap(_.toOption).map(_.doc.doc)
73 | }
74 |
75 | case class CouchAttachment(
76 | content_type: String,
77 | revpos: Int = -1,
78 | digest: String = "",
79 | data: String = "",
80 | length: Int = -1,
81 | stub: Boolean = false) {
82 | def toBytes: Array[Byte] = Base64.getDecoder.decode(data)
83 | }
84 |
85 | case object CouchAttachment {
86 | def fromBytes(data: Array[Byte], content_type: String = ""): CouchAttachment = {
87 | CouchAttachment(
88 | content_type = content_type,
89 | data = Base64.getEncoder.encodeToString(data))
90 | }
91 |
92 | def fromBytes(data: Array[Byte], content_type: `Content-Type`): CouchAttachment = {
93 | CouchAttachment(
94 | content_type = content_type.toString,
95 | data = Base64.getEncoder.encodeToString(data))
96 | }
97 | }
98 |
99 | case class CouchView(map: String, reduce: String = "")
100 |
101 | case class CouchDesign(
102 | name: String,
103 | _id: String = "",
104 | _rev: String = "",
105 | language: String = "javascript",
106 | validate_doc_update: String = "",
107 | views: Map[String, CouchView] = Map.empty[String, CouchView],
108 | shows: Map[String, String] = Map.empty[String, String],
109 | lists: Map[String, String] = Map.empty[String, String],
110 | _attachments: Map[String, CouchAttachment] = Map.empty[String, CouchAttachment],
111 | signatures: Map[String, String] = Map.empty[String, String])
112 |
113 | case class CouchException[D](content: D) extends Throwable {
114 | override def toString: String = "CouchException: " + content
115 | }
116 |
117 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/Req.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation, Google Inc.
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 |
17 | package com.ibm.couchdb
18 |
19 | object Req {
20 |
21 | case class Docs[D](docs: Seq[CouchDoc[D]])
22 |
23 | case class DocKeys[K](keys: Seq[K])
24 |
25 | case class ViewWithKeys[K](keys: Seq[K], view: CouchView)
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/Res.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb
18 |
19 | import org.http4s.Status
20 |
21 | import scalaz.concurrent.Task
22 |
23 | object Res {
24 |
25 | case class Ok(ok: Boolean = true)
26 |
27 | case class Error(
28 | error: String,
29 | reason: String,
30 | status: Status = Status.ExpectationFailed,
31 | request: String = "",
32 | requestBody: String = "") {
33 | def toTask[T]: Task[T] = Task.fail(CouchException(this))
34 | }
35 |
36 | case class ServerInfo(
37 | couchdb: String,
38 | uuid: String,
39 | version: String,
40 | vendor: ServerVendor)
41 |
42 | case class ServerVendor(version: String, name: String)
43 |
44 | case class DbInfo(
45 | committed_update_seq: Int,
46 | compact_running: Boolean,
47 | data_size: Int,
48 | db_name: String,
49 | disk_format_version: Int,
50 | disk_size: Int,
51 | doc_count: Int,
52 | doc_del_count: Int,
53 | instance_start_time: String,
54 | purge_seq: Int,
55 | update_seq: Int)
56 |
57 | case class ViewIndexInfo(
58 | compact_running: Boolean,
59 | data_size: Int,
60 | disk_size: Int,
61 | language: String,
62 | purge_seq: Int,
63 | signature: String,
64 | update_seq: Int,
65 | updater_running: Boolean,
66 | waiting_clients: Int,
67 | waiting_commit: Boolean)
68 |
69 | case class DesignInfo(name: String, view_index: ViewIndexInfo)
70 |
71 | case class Uuids(uuids: Seq[String])
72 |
73 | case class DocOk(ok: Boolean, id: String, rev: String)
74 | }
75 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/TypeMapping.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb
18 |
19 | final case class MappedDocType private(name: String)
20 |
21 | final class TypeMapping private(private val types: Map[String, String]) {
22 | def get(t: Class[_]): Option[MappedDocType] = {
23 | types.get(t.getCanonicalName).map(MappedDocType)
24 | }
25 |
26 | def contains(t: Class[_]): Boolean = {
27 | types.contains(t.getCanonicalName)
28 | }
29 |
30 | override def toString: String = types.toString
31 | }
32 |
33 | object TypeMapping {
34 | val empty = new TypeMapping(Map.empty[String, String])
35 |
36 | def apply(mapping: (Class[_], String)*): TypeMapping = {
37 | new TypeMapping(mapping.map((x: (Class[_], String)) => (x._1.getCanonicalName, x._2)).toMap)
38 | }
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/api/Databases.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.api
18 |
19 | import com.ibm.couchdb.Res
20 | import com.ibm.couchdb.core.Client
21 | import org.http4s.Status
22 |
23 | import scalaz.concurrent.Task
24 |
25 | class Databases(client: Client) {
26 |
27 | def get(name: String): Task[Res.DbInfo] = {
28 | client.get[Res.DbInfo](s"/$name", Status.Ok)
29 | }
30 |
31 | def getAll: Task[Seq[String]] = {
32 | client.get[Seq[String]]("/_all_dbs", Status.Ok)
33 | }
34 |
35 | def create(name: String): Task[Res.Ok] = {
36 | client.put[Res.Ok](s"/$name", Status.Created)
37 | }
38 |
39 | def delete(name: String): Task[Res.Ok] = {
40 | client.delete[Res.Ok](s"/$name", Status.Ok)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/api/Design.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.api
18 |
19 | import com.ibm.couchdb._
20 | import com.ibm.couchdb.core.Client
21 | import org.http4s.Status
22 |
23 | import scalaz.concurrent.Task
24 |
25 | class Design(client: Client, db: String) {
26 |
27 | def create(design: CouchDesign): Task[Res.DocOk] = {
28 | if (design.name.isEmpty)
29 | Res.Error("cannot_create", "Design name must not be empty").toTask
30 | else
31 | client.put[CouchDesign, Res.DocOk](
32 | s"/$db/_design/${design.name}",
33 | Status.Created,
34 | design)
35 | }
36 |
37 | def info(name: String): Task[Res.DesignInfo] = {
38 | if (name.isEmpty)
39 | Res.Error("not_found", "Design name must not be empty").toTask
40 | else
41 | client.get[Res.DesignInfo](
42 | s"/$db/_design/$name/_info",
43 | Status.Ok)
44 | }
45 |
46 | def get(name: String): Task[CouchDesign] = {
47 | if (name.isEmpty)
48 | Res.Error("not_found", "Design name must not be empty").toTask
49 | else
50 | client.get[CouchDesign](
51 | s"/$db/_design/$name",
52 | Status.Ok
53 | )
54 | }
55 |
56 | def getWithAttachments(name: String): Task[CouchDesign] = {
57 | if (name.isEmpty)
58 | Res.Error("not_found", "Design name must not be empty").toTask
59 | else
60 | client.get[CouchDesign](
61 | s"/$db/_design/$name?attachments=true",
62 | Status.Ok
63 | )
64 | }
65 |
66 | def getById(id: String): Task[CouchDesign] = {
67 | if (id.isEmpty)
68 | Res.Error("not_found", "Design ID must not be empty").toTask
69 | else
70 | client.get[CouchDesign](
71 | s"/$db/$id",
72 | Status.Ok
73 | )
74 | }
75 |
76 | def update(design: CouchDesign): Task[Res.DocOk] = {
77 | if (design._id.isEmpty)
78 | Res.Error("cannot_update", "Design ID must not be empty").toTask
79 | else
80 | client.put[CouchDesign, Res.DocOk](
81 | s"/$db/${design._id}",
82 | Status.Created,
83 | design)
84 | }
85 |
86 | def delete(design: CouchDesign): Task[Res.DocOk] = {
87 | if (design._id.isEmpty)
88 | Res.Error("cannot_delete", "Design ID must not be empty").toTask
89 | else
90 | client.delete[Res.DocOk](
91 | s"/$db/${design._id}?rev=${design._rev}",
92 | Status.Ok)
93 | }
94 |
95 | def deleteByName(name: String): Task[Res.DocOk] = {
96 | if (name.isEmpty)
97 | Res.Error("cannot_delete", "Design name must not be empty").toTask
98 | else
99 | get(name) flatMap delete
100 | }
101 |
102 | def attach(
103 | design: CouchDesign,
104 | name: String,
105 | data: Array[Byte],
106 | contentType: String = ""): Task[Res.DocOk] = {
107 | if (design._id.isEmpty)
108 | Res.Error("cannot_attach", "Design ID must not be empty").toTask
109 | else
110 | client.put[Res.DocOk](
111 | s"/$db/${design._id}/$name?rev=${design._rev}",
112 | Status.Created,
113 | data,
114 | contentType)
115 | }
116 |
117 | def getAttachment(design: CouchDesign, name: String): Task[Array[Byte]] = {
118 | if (design._id.isEmpty)
119 | Res.Error("not_found", "Design ID must not be empty").toTask
120 | else
121 | client.getBinary(
122 | s"/$db/${design._id}/$name",
123 | Status.Ok)
124 | }
125 |
126 | def deleteAttachment(design: CouchDesign, name: String): Task[Res.DocOk] = {
127 | if (design._id.isEmpty)
128 | Res.Error("cannot_delete", "Design ID must not be empty").toTask
129 | else if (name.isEmpty)
130 | Res.Error("cannot_delete", "Attachment name must not be empty").toTask
131 | else
132 | client.delete[Res.DocOk](
133 | s"/$db/${design._id}/$name?rev=${design._rev}",
134 | Status.Ok)
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/api/Documents.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation, Google Inc.
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 |
17 | package com.ibm.couchdb.api
18 |
19 | import java.nio.file.{Files, Paths}
20 |
21 | import com.ibm.couchdb._
22 | import com.ibm.couchdb.api.builders._
23 | import com.ibm.couchdb.core.Client
24 | import org.http4s.Status
25 | import upickle.default.Aliases.{R, W}
26 |
27 | import scalaz.concurrent.Task
28 |
29 | class Documents(client: Client, db: String, typeMapping: TypeMapping) {
30 |
31 | val server = new Server(client)
32 |
33 | def create[D: W](obj: D): Task[Res.DocOk] = {
34 | server.mkUuid flatMap (create(obj, _))
35 | }
36 |
37 | def create[D: W](obj: D, attachments: Map[String, CouchAttachment]): Task[Res.DocOk] = {
38 | server.mkUuid flatMap (create(obj, _, attachments))
39 | }
40 |
41 | def create[D: W](
42 | obj: D, id: String,
43 | attachments: Map[String, CouchAttachment] = Map.empty): Task[Res.DocOk] = {
44 | typeMapping.get(obj.getClass) match {
45 | case Some(t) =>
46 | client.put[CouchDoc[D], Res.DocOk](
47 | s"/$db/$id",
48 | Status.Created,
49 | CouchDoc[D](obj, t.name, _attachments = attachments))
50 | case None =>
51 | val cl = obj.getClass.getCanonicalName
52 | Res.Error(
53 | "cannot_create", "No type mapping for " + cl + " available: " + typeMapping).toTask
54 | }
55 | }
56 |
57 | private def postBulk[D: W](objs: Seq[CouchDoc[D]]): Task[Seq[Res.DocOk]] = {
58 | client.post[Req.Docs[D], Seq[Res.DocOk]](
59 | s"/$db/_bulk_docs",
60 | Status.Created, Req.Docs(objs))
61 | }
62 |
63 | def createMany[D: W](objs: Map[String, D]): Task[Seq[Res.DocOk]] = create(objs.toSeq)
64 |
65 | def createMany[D: W](objs: Seq[D]): Task[Seq[Res.DocOk]] = create(objs.map(("", _)))
66 |
67 | private def create[D: W, S](objs: Seq[(String, D)]): Task[Seq[Res.DocOk]] = {
68 | objs.find { x => !typeMapping.contains(x._2.getClass) } match {
69 | case Some(missing) =>
70 | Res.Error("cannot_create", "No type mapping for " + missing).toTask
71 | case None =>
72 | postBulk(
73 | objs.map { o => CouchDoc[D](
74 | _id = o._1,
75 | doc = o._2,
76 | kind = typeMapping.get(o._2.getClass).fold("")(_.name))
77 | })
78 | }
79 | }
80 |
81 | def updateMany[D: W](objs: Seq[CouchDoc[D]]): Task[Seq[Res.DocOk]] = {
82 | def invalidDoc(x: CouchDoc[D]): Boolean = x._id.isEmpty || x._rev.isEmpty
83 | objs.find(invalidDoc) match {
84 | case Some(doc) =>
85 | val missingField = if (doc._id.isEmpty) "an ID" else "a REV number"
86 | Res.Error("cannot_update", s"One or more documents do not contain $missingField.").toTask
87 | case None => postBulk(objs)
88 | }
89 | }
90 |
91 | def deleteMany[D: W](objs: Seq[CouchDoc[D]]): Task[Seq[Res.DocOk]] = {
92 | updateMany(objs.map(_.copy(_deleted = true)))
93 | }
94 |
95 | def get: GetDocumentQueryBuilder = GetDocumentQueryBuilder(client, db)
96 |
97 | def get[D: R](id: String): Task[CouchDoc[D]] = {
98 | get.query[D](id)
99 | }
100 |
101 | def getMany: GetManyDocumentsQueryBuilder[ExcludeDocs, MissingDisallowed, AnyDocType] =
102 | GetManyDocumentsQueryBuilder(client, db, typeMapping)
103 |
104 | def getMany[D: R](ids: Seq[String]): Task[CouchDocs[String, CouchDocRev, D]] = {
105 | getMany.includeDocs[D].withIds(ids).build.query
106 | }
107 |
108 | def update[D: W](obj: CouchDoc[D]): Task[Res.DocOk] = {
109 | if (obj._id.isEmpty)
110 | Res.Error("cannot_update", "Document ID must not be empty").toTask
111 | else
112 | client.put[CouchDoc[D], Res.DocOk](
113 | s"/$db/${obj._id}",
114 | Status.Created,
115 | obj)
116 | }
117 |
118 | def delete[D](obj: CouchDoc[D]): Task[Res.DocOk] = {
119 | if (obj._id.isEmpty)
120 | Res.Error("cannot_delete", "Document ID must not be empty").toTask
121 | else {
122 | client.delete[Res.DocOk](
123 | s"/$db/${obj._id}?rev=${obj._rev}",
124 | Status.Ok)
125 | }
126 | }
127 |
128 | def attach[D](
129 | obj: CouchDoc[D],
130 | name: String,
131 | data: Array[Byte],
132 | contentType: String = ""): Task[Res.DocOk] = {
133 | if (obj._id.isEmpty)
134 | Res.Error("cannot_attach", "Document ID must not be empty").toTask
135 | else {
136 | client.put[Res.DocOk](
137 | s"/$db/${obj._id}/$name?rev=${obj._rev}",
138 | Status.Created,
139 | data,
140 | contentType)
141 | }
142 | }
143 |
144 | def attach[D](obj: CouchDoc[D], name: String, path: String): Task[Res.DocOk] = {
145 | readFile(path) flatMap {
146 | attachment => attach(obj, name, attachment)
147 | }
148 | }
149 |
150 | def getAttachmentResource[D](obj: CouchDoc[D], name: String): Task[String] = {
151 | if (obj._id.isEmpty)
152 | Res.Error("not_found", "Document ID must not be empty").toTask
153 | else
154 | Task.now(s"/$db/${obj._id}/$name")
155 | }
156 |
157 | def getAttachmentUrl[D](obj: CouchDoc[D], name: String): Task[String] = {
158 | getAttachmentResource(obj, name).map(client.url(_).toString())
159 | }
160 |
161 | def getAttachment[D](obj: CouchDoc[D], name: String): Task[Array[Byte]] = {
162 | getAttachmentResource(obj, name).flatMap(client.getBinary(_, Status.Ok))
163 | }
164 |
165 | def deleteAttachment[D](obj: CouchDoc[D], name: String): Task[Res.DocOk] = {
166 | if (obj._id.isEmpty)
167 | Res.Error("cannot_delete", "Document ID must not be empty").toTask
168 | else if (name.isEmpty)
169 | Res.Error("cannot_delete", "The attachment name is empty").toTask
170 | else
171 | client.delete[Res.DocOk](
172 | s"/$db/${obj._id}/$name?rev=${obj._rev}",
173 | Status.Ok)
174 | }
175 |
176 | private def readFile(path: String): Task[Array[Byte]] = Task {
177 | // TODO: replace with scodec / scalaz-stream / scodec-stream?
178 | Files.readAllBytes(Paths.get(path))
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/api/Query.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.api
18 |
19 | import com.ibm.couchdb.CouchView
20 | import com.ibm.couchdb.api.builders._
21 | import com.ibm.couchdb.core.Client
22 | import upickle.default.Aliases.{R, W}
23 |
24 | import scalaz.Scalaz._
25 |
26 | class Query(client: Client, db: String) {
27 |
28 | def view[K: R, V: R](design: String, view: String)(implicit kw: W[K]):
29 | Option[ViewQueryBuilder[K, V, ExcludeDocs, MapOnly]] = {
30 | if (design.isEmpty || view.isEmpty)
31 | none[ViewQueryBuilder[K, V, ExcludeDocs, MapOnly]]
32 | else ViewQueryBuilder[K, V](client, db, design, view).some
33 | }
34 |
35 | def temporaryView[K: R, V: R](view: CouchView)(implicit kw: W[K]):
36 | Option[ViewQueryBuilder[K, V, ExcludeDocs, MapOnly]] = {
37 | if (view.map.isEmpty)
38 | none[ViewQueryBuilder[K, V, ExcludeDocs, MapOnly]]
39 | else ViewQueryBuilder[K, V](client, db, view).some
40 | }
41 |
42 | def show(design: String, show: String): Option[ShowQueryBuilder] = {
43 | if (design.isEmpty || show.isEmpty) none[ShowQueryBuilder]
44 | else ShowQueryBuilder(client, db, design, show).some
45 | }
46 |
47 | def list(design: String, list: String): Option[ListQueryBuilder] = {
48 | if (design.isEmpty || list.isEmpty) none[ListQueryBuilder]
49 | else ListQueryBuilder(client, db, design, list).some
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/api/Server.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.api
18 |
19 | import com.ibm.couchdb.Res
20 | import com.ibm.couchdb.core.Client
21 | import org.http4s.Status
22 |
23 | import scalaz.concurrent.Task
24 |
25 | class Server(client: Client) {
26 |
27 | def info: Task[Res.ServerInfo] = {
28 | client.get[Res.ServerInfo]("/", Status.Ok)
29 | }
30 |
31 | def mkUuid: Task[String] = {
32 | client.get[Res.Uuids]("/_uuids", Status.Ok).map(_.uuids(0))
33 | }
34 |
35 | def mkUuids(count: Int): Task[Seq[String]] = {
36 | client.get[Res.Uuids](s"/_uuids?count=$count", Status.Ok).map(_.uuids)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/api/builders/GetDocumentQueryBuilder.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation, Google Inc.
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 |
17 | package com.ibm.couchdb.api.builders
18 |
19 | import com.ibm.couchdb._
20 | import com.ibm.couchdb.core.Client
21 | import org.http4s.Status
22 | import upickle.default.Aliases.R
23 | import upickle.default.write
24 |
25 | import scalaz.concurrent.Task
26 |
27 | case class GetDocumentQueryBuilder(
28 | client: Client, db: String, params: Map[String, String] = Map.empty[String, String]) {
29 |
30 | def attachments(attachments: Boolean = true): GetDocumentQueryBuilder = {
31 | set("attachments", attachments)
32 | }
33 |
34 | def attEncodingInfo(attEncodingInfo: Boolean = true): GetDocumentQueryBuilder = {
35 | set("att_encoding_info", attEncodingInfo)
36 | }
37 |
38 | def attsSince(attsSince: Seq[String]): GetDocumentQueryBuilder = {
39 | set("atts_since", write(attsSince))
40 | }
41 |
42 | def conflicts(conflicts: Boolean = true): GetDocumentQueryBuilder = {
43 | set("conflicts", conflicts)
44 | }
45 |
46 | def deletedConflicts(deletedConflicts: Boolean = true): GetDocumentQueryBuilder = {
47 | set("deleted_conflicts", deletedConflicts)
48 | }
49 |
50 | def latest(latest: Boolean = true): GetDocumentQueryBuilder = {
51 | set("latest", latest)
52 | }
53 |
54 | def localSeq(localSeq: Boolean = true): GetDocumentQueryBuilder = {
55 | set("local_seq", localSeq)
56 | }
57 |
58 | def meta(meta: Boolean = true): GetDocumentQueryBuilder = {
59 | set("meta", meta)
60 | }
61 |
62 | def openRevs(openRevs: Seq[String]): GetDocumentQueryBuilder = {
63 | set("open_revs", write(openRevs))
64 | }
65 |
66 | def openRevs(openRevs: String): GetDocumentQueryBuilder = {
67 | set("open_revs", openRevs)
68 | }
69 |
70 | def rev(rev: String): GetDocumentQueryBuilder = {
71 | set("rev", rev)
72 | }
73 |
74 | def revs(revs: Boolean = true): GetDocumentQueryBuilder = {
75 | set("revs", revs)
76 | }
77 |
78 | def revsInfo(revsInfo: Boolean = true): GetDocumentQueryBuilder = {
79 | set("revs_info", revsInfo)
80 | }
81 |
82 | private def set(key: String, value: String): GetDocumentQueryBuilder = {
83 | copy(params = params.updated(key, value))
84 | }
85 |
86 | private def set(key: String, value: Any): GetDocumentQueryBuilder = {
87 | set(key, value.toString)
88 | }
89 |
90 | def query[D: R](id: String): Task[CouchDoc[D]] = {
91 | if (id.isEmpty)
92 | Res.Error("not_found", "No ID specified").toTask
93 | else
94 | client.get[CouchDoc[D]](
95 | s"/$db/$id",
96 | Status.Ok,
97 | params.toSeq)
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/api/builders/GetManyDocumentsQueryBuilder.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.api.builders
18 |
19 | import com.ibm.couchdb._
20 | import com.ibm.couchdb.core.Client
21 | import upickle.default.Aliases.{R, W}
22 | import upickle.default.write
23 |
24 | import scala.reflect.ClassTag
25 |
26 | sealed trait DocsInResult
27 | abstract class IncludeDocs[D: R] extends DocsInResult
28 | trait ExcludeDocs extends DocsInResult
29 |
30 | sealed trait MissingIdsInQuery
31 | trait MissingAllowed extends MissingIdsInQuery
32 | trait MissingDisallowed extends MissingIdsInQuery
33 |
34 | sealed trait DocType
35 | abstract class ForDocType[D: R, K: R, V: R] extends DocType
36 | trait AnyDocType extends DocType
37 |
38 | case class GetManyDocumentsQueryBuilder[ID <: DocsInResult, AM <: MissingIdsInQuery,
39 | BT <: DocType] private(
40 | client: Client,
41 | db: String,
42 | typeMappings: TypeMapping,
43 | params: Map[String, String] = Map.empty[String, String],
44 | ids: Seq[String] = Seq.empty, view: Option[CouchView] = None) {
45 |
46 | private[builders] val url: String = s"/$db/_all_docs"
47 |
48 | lazy val tempTypeFilterView: CouchView = {
49 | CouchView(
50 | map =
51 | """
52 | |function(doc) {
53 | | emit([doc.kind, doc._id], doc._id);
54 | |}
55 | """.stripMargin)
56 | }
57 |
58 | def conflicts(
59 | conflicts: Boolean = true): GetManyDocumentsQueryBuilder[ID, AM, BT] = {
60 | set("conflicts", conflicts)
61 | }
62 |
63 | def descending(
64 | descending: Boolean = true): GetManyDocumentsQueryBuilder[ID, AM, BT] = {
65 | set("descending", descending)
66 | }
67 |
68 | def endKey[K: W](endKey: K): GetManyDocumentsQueryBuilder[ID, AM, BT] = {
69 | set("endkey", write(endKey))
70 | }
71 |
72 | def endKeyDocId(
73 | endKeyDocId: String): GetManyDocumentsQueryBuilder[ID, AM, BT] = {
74 | set("endkey_docid", endKeyDocId)
75 | }
76 |
77 | def includeDocs[D: R]: GetManyDocumentsQueryBuilder[IncludeDocs[D], AM, BT] = {
78 | set("include_docs", true)
79 | }
80 |
81 | def excludeDocs: GetManyDocumentsQueryBuilder[ExcludeDocs, AM, BT] = {
82 | set("include_docs", false)
83 | }
84 |
85 | def allowMissing: GetManyDocumentsQueryBuilder[ID, MissingAllowed, BT] = {
86 | setType()
87 | }
88 |
89 | def disallowMissing: GetManyDocumentsQueryBuilder[ID, MissingDisallowed, BT] = {
90 | setType()
91 | }
92 |
93 | def withIds(ids: Seq[String]): GetManyDocumentsQueryBuilder[ID, AM, BT] = {
94 | set(params, ids, view)
95 | }
96 |
97 | def byType[K: R, V: R](view: String, design: String, mappedType: MappedDocType)
98 | (implicit kw: W[K]): ViewQueryBuilder[K, V, ID, MapOnly] = {
99 | new ViewQueryBuilder[K, V, ID, MapOnly](
100 | client, db, Option(design), Option(view), params = params).
101 | startKey(Tuple1(mappedType.name)).endKey(Tuple2(mappedType.name, {}))
102 | }
103 |
104 | def byType[V: R](view: String, design: String, mappedType: MappedDocType):
105 | ViewQueryBuilder[(String, String), V, ID, MapOnly] = {
106 | byType[(String, String), V](view, design, mappedType)
107 | }
108 |
109 | def byTypeUsingTemporaryView(mappedType: MappedDocType):
110 | ViewQueryBuilder[(String, String), String, ID, MapOnly] = {
111 | new ViewQueryBuilder[(String, String), String, ID, MapOnly](
112 | client, db, None, None, temporaryView = Option(tempTypeFilterView), params = params).
113 | startKey(Tuple1(mappedType.name)).endKey(Tuple2(mappedType.name, {}))
114 | }
115 |
116 | def inclusiveEnd(inclusiveEnd: Boolean = true): GetManyDocumentsQueryBuilder[ID, AM, BT] = {
117 | set("inclusive_end", inclusiveEnd)
118 | }
119 |
120 | def key[K: W](key: K): GetManyDocumentsQueryBuilder[ID, AM, BT] = {
121 | set("key", write(key))
122 | }
123 |
124 | def limit(limit: Int): GetManyDocumentsQueryBuilder[ID, AM, BT] = {
125 | set("limit", limit)
126 | }
127 |
128 | def skip(skip: Int): GetManyDocumentsQueryBuilder[ID, AM, BT] = {
129 | set("skip", skip)
130 | }
131 |
132 | def stale(stale: String): GetManyDocumentsQueryBuilder[ID, AM, BT] = {
133 | set("stale", stale)
134 | }
135 |
136 | def startKey[K: W](
137 | startKey: K): GetManyDocumentsQueryBuilder[ID, AM, BT] = {
138 | set("startkey", write(startKey))
139 | }
140 |
141 | def startKeyDocId(
142 | startKeyDocId: String): GetManyDocumentsQueryBuilder[ID, AM, BT] = {
143 | set("startkey_docid", startKeyDocId)
144 | }
145 |
146 | def updateSeq(
147 | updateSeq: Boolean = true): GetManyDocumentsQueryBuilder[ID, AM, BT] = {
148 | set("update_seq", updateSeq)
149 | }
150 |
151 | private def setType[I <: DocsInResult, A <: MissingIdsInQuery, B <: DocType]():
152 | GetManyDocumentsQueryBuilder[I, A, B] = {
153 | set[I, A, B](params, ids, view)
154 | }
155 |
156 | private def set[I <: DocsInResult, A <: MissingIdsInQuery, B <: DocType]
157 | (_params: Map[String, String], _ids: Seq[String], _view: Option[CouchView]):
158 | GetManyDocumentsQueryBuilder[I, A, B] = {
159 | new GetManyDocumentsQueryBuilder(client, db, typeMappings, _params, _ids, _view)
160 | }
161 |
162 | private def set[I <: DocsInResult, A <: MissingIdsInQuery, B <: DocType](
163 | key: String, value: String): GetManyDocumentsQueryBuilder[I, A, B] = {
164 | set(params.updated(key, value), ids, view)
165 | }
166 |
167 | private def set[I <: DocsInResult, A <: MissingIdsInQuery, B <: DocType](
168 | key: String, value: Any): GetManyDocumentsQueryBuilder[I, A, B] = {
169 | set(key, value.toString)
170 | }
171 | }
172 |
173 | object GetManyDocumentsQueryBuilder {
174 |
175 | private type MDBuilder[ID <: DocsInResult, AM <: MissingIdsInQuery, BT <: DocType] =
176 | GetManyDocumentsQueryBuilder[ID, AM, BT]
177 |
178 | case class Builder[T: R, ID <: DocsInResult, AM <: MissingIdsInQuery]
179 | (builder: MDBuilder[ID, AM, AnyDocType]) {
180 | def build: QueryBasic[T] = QueryBasic(
181 | builder.client, builder.db, builder.url,
182 | builder.params, builder.ids)
183 | }
184 |
185 | case class ByTypeBuilder[K: R, V: R, D: R](
186 | builder: MDBuilder[IncludeDocs[D], MissingDisallowed, ForDocType[K, V, D]])
187 | (implicit tag: ClassTag[D], kw: W[K]) {
188 | def build: QueryByType[K, V, D] = {
189 | val view = builder.view.getOrElse(builder.tempTypeFilterView)
190 | QueryByType(builder.client, builder.db, view, builder.typeMappings)
191 | }
192 | }
193 |
194 | private type BasicBuilder = Builder[CouchKeyVals[String, CouchDocRev], ExcludeDocs,
195 | MissingDisallowed]
196 |
197 | private type AllowMissingBuilder = Builder[CouchKeyValsIncludesMissing[String, CouchDocRev],
198 | ExcludeDocs, MissingAllowed]
199 |
200 | private type IncludeDocsBuilder[D] = Builder[CouchDocs[String, CouchDocRev, D], IncludeDocs[D],
201 | MissingDisallowed]
202 |
203 | private type AllowMissingIncludeDocsBuilder[D] = Builder[CouchDocsIncludesMissing[String,
204 | CouchDocRev, D], IncludeDocs[D], MissingAllowed]
205 |
206 | implicit def buildBasic(builder: MDBuilder[ExcludeDocs, MissingDisallowed, AnyDocType]):
207 | BasicBuilder = new BasicBuilder(builder)
208 |
209 | implicit def buildAllowMissing(builder: MDBuilder[ExcludeDocs, MissingAllowed, AnyDocType]):
210 | AllowMissingBuilder = new AllowMissingBuilder(builder)
211 |
212 | implicit def buildIncludeDocs[D: R](
213 | builder: MDBuilder[IncludeDocs[D], MissingDisallowed, AnyDocType]): IncludeDocsBuilder[D] =
214 | new IncludeDocsBuilder(builder)
215 |
216 | implicit def buildIncludeDocsAllowMissing[D: R](
217 | builder: MDBuilder[IncludeDocs[D], MissingAllowed, AnyDocType]):
218 | AllowMissingIncludeDocsBuilder[D] = new AllowMissingIncludeDocsBuilder(builder)
219 |
220 | implicit def buildByTypeIncludeDocs[K: R, V: R, D: R](
221 | builder: MDBuilder[IncludeDocs[D], MissingDisallowed, ForDocType[K, V, D]])
222 | (implicit tag: ClassTag[D], kw: W[K]): ByTypeBuilder[K, V, D] = {
223 | ByTypeBuilder(builder)
224 | }
225 |
226 | def apply(client: Client, db: String, typeMapping: TypeMapping):
227 | MDBuilder[ExcludeDocs, MissingDisallowed, AnyDocType] =
228 | new MDBuilder(client, db, typeMapping)
229 | }
230 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/api/builders/ListQueryBuilder.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.api.builders
18 |
19 | import com.ibm.couchdb.Res
20 | import com.ibm.couchdb.core.Client
21 | import org.http4s.Status
22 | import upickle.default.Aliases.W
23 | import upickle.default.write
24 |
25 | import scalaz.concurrent.Task
26 |
27 | case class ListQueryBuilder(
28 | client: Client,
29 | db: String,
30 | design: String,
31 | list: String,
32 | params: Map[String, String] = Map.empty[String, String]) {
33 |
34 | def format(format: String): ListQueryBuilder = {
35 | set("format", format)
36 | }
37 |
38 | def descending(descending: Boolean = true): ListQueryBuilder = {
39 | set("descending", descending)
40 | }
41 |
42 | def endKey[K: W](endKey: K): ListQueryBuilder = {
43 | set("endkey", write(endKey))
44 | }
45 |
46 | def endKeyDocId(endKeyDocId: String): ListQueryBuilder = {
47 | set("endkey_docid", endKeyDocId)
48 | }
49 |
50 | def group(group: Boolean = true): ListQueryBuilder = {
51 | set("group", group)
52 | }
53 |
54 | def groupLevel(groupLevel: Int): ListQueryBuilder = {
55 | set("group_level", groupLevel)
56 | }
57 |
58 | def inclusiveEnd(inclusiveEnd: Boolean = true): ListQueryBuilder = {
59 | set("inclusive_end", inclusiveEnd)
60 | }
61 |
62 | def key[K: W](key: K): ListQueryBuilder = {
63 | set("key", write(key))
64 | }
65 |
66 | def limit(limit: Int): ListQueryBuilder = {
67 | set("limit", limit)
68 | }
69 |
70 | def reduce(reduce: Boolean = true): ListQueryBuilder = {
71 | set("reduce", reduce)
72 | }
73 |
74 | def skip(skip: Int): ListQueryBuilder = {
75 | set("skip", skip)
76 | }
77 |
78 | def stale(stale: String): ListQueryBuilder = {
79 | set("stale", stale)
80 | }
81 |
82 | def startKey[K: W](startKey: K): ListQueryBuilder = {
83 | set("startkey", write(startKey))
84 | }
85 |
86 | def startKeyDocId(startKeyDocId: String): ListQueryBuilder = {
87 | set("startkey_docid", startKeyDocId)
88 | }
89 |
90 | def updateSeq(updateSeq: Boolean = true): ListQueryBuilder = {
91 | set("update_seq", updateSeq)
92 | }
93 |
94 | def addParam(name: String, value: String): ListQueryBuilder = {
95 | set(name, value)
96 | }
97 |
98 | private def set(key: String, value: String): ListQueryBuilder = {
99 | copy(params = params.updated(key, value))
100 | }
101 |
102 | private def set(key: String, value: Any): ListQueryBuilder = {
103 | set(key, value.toString)
104 | }
105 |
106 | def query(view: String): Task[String] = {
107 | if (view.isEmpty)
108 | Res.Error("not_found", "View name must not be empty").toTask
109 | else
110 | client.getRaw(
111 | s"/$db/_design/$design/_list/$list/$view",
112 | Status.Ok,
113 | params.toSeq)
114 | }
115 |
116 | def queryAnotherDesign(view: String, anotherDesign: String): Task[String] = {
117 | if (view.isEmpty)
118 | Res.Error("not_found", "View name must not be empty").toTask
119 | else
120 | client.getRaw(
121 | s"/$db/_design/$anotherDesign/_list/$list/$view",
122 | Status.Ok,
123 | params.toSeq)
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/api/builders/QueryOps.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 IBM Corporation
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 |
17 | package com.ibm.couchdb.api.builders
18 |
19 | import com.ibm.couchdb.core.Client
20 | import com.ibm.couchdb.Req
21 | import org.http4s.Status
22 | import upickle.default.Aliases.{R, W}
23 | import upickle.default._
24 |
25 | import scalaz.concurrent.Task
26 |
27 | case class QueryOps(client: Client) {
28 |
29 | def query[Q: R](
30 | url: String,
31 | params: Map[String, String]): Task[Q] = {
32 | client.get[Q](url, Status.Ok, params.toSeq)
33 | }
34 |
35 | def queryByIds[K: W, Q: R](
36 | url: String,
37 | ids: Seq[K],
38 | params: Map[String, String]): Task[Q] = {
39 | postQuery[Req.DocKeys[K], Q](url, Req.DocKeys(ids), params)
40 | }
41 |
42 | def postQuery[B: W, Q: R](
43 | url: String,
44 | body: B,
45 | params: Map[String, String]): Task[Q] = {
46 | client.post[B, Q](url, Status.Ok, body, params.toSeq)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/api/builders/QueryStrategy.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 IBM Corporation
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 |
17 | package com.ibm.couchdb.api.builders
18 |
19 | import com.ibm.couchdb.api.Query
20 | import com.ibm.couchdb.core.Client
21 | import com.ibm.couchdb.{CouchDocs, CouchView, Req, Res, TypeMapping}
22 | import upickle.default.Aliases.{R, W}
23 | import upickle.default._
24 | import com.ibm.couchdb._
25 |
26 | import scala.reflect.ClassTag
27 | import scalaz.concurrent.Task
28 |
29 | case class QueryBasic[C: R](
30 | client: Client, db: String, url: String, params: Map[String, String] = Map.empty,
31 | ids: Seq[String] = Seq.empty) {
32 |
33 | private val queryOps = QueryOps(client)
34 |
35 | def query: Task[C] = {
36 | ids match {
37 | case Nil => queryOps.query[C](url, params)
38 | case _ => queryOps.queryByIds[String, C](url, ids, params)
39 | }
40 | }
41 | }
42 |
43 | case class QueryView[K: W, C: R](
44 | client: Client, db: String, design: Option[String], params: Map[String, String] = Map.empty,
45 | ids: Seq[K] = Seq.empty, view: Option[String], tempView: Option[CouchView]) {
46 |
47 | private lazy val url = (view, design) match {
48 | case (Some(v), Some(d)) => s"/$db/_design/$d/_view/$v"
49 | case _ => s"/$db/_temp_view"
50 | }
51 |
52 | private val queryOps = QueryOps(client)
53 |
54 | def query: Task[C] = {
55 | ids match {
56 | case Nil => queryWithoutIds
57 | case _ => queryByIds
58 | }
59 | }
60 |
61 | private def queryWithoutIds: Task[C] = tempView match {
62 | case Some(t) => queryOps.postQuery[CouchView, C](url, t, params)
63 | case None => queryOps.query[C](url, params)
64 | }
65 |
66 | private def queryByIds: Task[C] = tempView match {
67 | case Some(t) => queryOps.postQuery[Req.ViewWithKeys[K], C](
68 | url, Req.ViewWithKeys(ids, t), params)
69 | case None => queryOps.queryByIds[K, C](url, ids, params)
70 | }
71 | }
72 |
73 | case class QueryByType[K, V, D: R](
74 | client: Client, db: String, typeFilterView: CouchView,
75 | typeMappings: TypeMapping, params: Map[String, String] = Map.empty,
76 | ids: Seq[String] = Seq.empty)
77 | (implicit tag: ClassTag[D], kr: R[K], kw: W[K], vr: R[V]) {
78 |
79 | def query: Task[CouchDocs[K, V, D]] = {
80 | typeMappings.get(tag.runtimeClass) match {
81 | case Some(k) => queryByType(typeFilterView, k.name)
82 | case None => Res.Error("not_found", s"type mapping not found").toTask
83 | }
84 | }
85 |
86 | private def queryByType(view: CouchView, kind: String, ps: Map[String, String] = Map.empty) = {
87 | new Query(client, db).temporaryView[K, V](view) match {
88 | case Some(v) => v.startKey(Tuple1(kind)).endKey(Tuple2(kind, {})).includeDocs[D].build.query
89 | case None => Res.Error("not_found", "invalid view specified").toTask
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/api/builders/ShowQueryBuilder.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.api.builders
18 |
19 | import com.ibm.couchdb.Res
20 | import com.ibm.couchdb.core.Client
21 | import org.http4s.Status
22 |
23 | import scalaz.concurrent.Task
24 |
25 | case class ShowQueryBuilder(
26 | client: Client,
27 | db: String,
28 | design: String,
29 | show: String,
30 | params: Map[String, String] = Map.empty[String, String]) {
31 |
32 | def details(details: Boolean = true): ShowQueryBuilder = {
33 | set("details", details)
34 | }
35 |
36 | def format(format: String): ShowQueryBuilder = {
37 | set("format", format)
38 | }
39 |
40 | def addParam(name: String, value: String): ShowQueryBuilder = {
41 | set(name, value)
42 | }
43 |
44 | private def set(key: String, value: String): ShowQueryBuilder = {
45 | copy(params = params.updated(key, value))
46 | }
47 |
48 | private def set(key: String, value: Any): ShowQueryBuilder = {
49 | set(key, value.toString)
50 | }
51 |
52 | def query: Task[String] = {
53 | client.getRaw(
54 | s"/$db/_design/$design/_show/$show",
55 | Status.Ok,
56 | params.toSeq)
57 | }
58 |
59 | def query(id: String): Task[String] = {
60 | if (id.isEmpty)
61 | Res.Error("not_found", "Document ID must not be empty").toTask
62 | else
63 | client.getRaw(
64 | s"/$db/_design/$design/_show/$show/$id",
65 | Status.Ok,
66 | params.toSeq)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/api/builders/ViewQueryBuilder.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.api.builders
18 |
19 | import com.ibm.couchdb._
20 | import com.ibm.couchdb.core.Client
21 | import upickle.default.Aliases.{R, W}
22 | import upickle.default.write
23 |
24 | sealed trait ViewOperation
25 | abstract class MapWithReduce[A: R] extends ViewOperation
26 | trait MapOnly extends ViewOperation
27 |
28 | case class ViewQueryBuilder[K: R, V: R, DR <: DocsInResult, MR <: ViewOperation] private[builders](
29 | client: Client,
30 | db: String,
31 | design: Option[String],
32 | view: Option[String],
33 | temporaryView: Option[CouchView] = None,
34 | params: Map[String, String] = Map.empty[String, String],
35 | ids: Seq[K] = Seq.empty)(implicit kw: W[K], ckr: R[CouchKeyVals[K, V]]) {
36 |
37 | def conflicts(conflicts: Boolean = true): ViewQueryBuilder[K, V, DR, MR] = {
38 | set("conflicts", conflicts)
39 | }
40 |
41 | def descending(descending: Boolean = true): ViewQueryBuilder[K, V, DR, MR] = {
42 | set("descending", descending)
43 | }
44 |
45 | def endKey[K2: W](endKey: K2): ViewQueryBuilder[K, V, DR, MR] = {
46 | set("endkey", write(endKey))
47 | }
48 |
49 | def endKeyDocId(endKeyDocId: String): ViewQueryBuilder[K, V, DR, MR] = {
50 | set("endkey_docid", endKeyDocId)
51 | }
52 |
53 | def group(group: Boolean = true): ViewQueryBuilder[K, V, DR, MR] = {
54 | set("group", group)
55 | }
56 |
57 | def groupLevel(groupLevel: Int): ViewQueryBuilder[K, V, DR, MR] = {
58 | set("group_level", groupLevel)
59 | }
60 |
61 | def includeDocs[D: R]: ViewQueryBuilder[K, V, IncludeDocs[D], MR] = {
62 | set("include_docs", true)
63 | }
64 |
65 | def excludeDocs: ViewQueryBuilder[K, V, ExcludeDocs, MR] = {
66 | set("include_docs", false)
67 | }
68 |
69 | def attachments(attachments: Boolean = true): ViewQueryBuilder[K, V, DR, MR] = {
70 | set("attachments", attachments)
71 | }
72 |
73 | def attEncodingInfo(attEncodingInfo: Boolean = true): ViewQueryBuilder[K, V, DR, MR] = {
74 | set("att_encoding_info", attEncodingInfo)
75 | }
76 |
77 | def inclusiveEnd(inclusiveEnd: Boolean = true): ViewQueryBuilder[K, V, DR, MR] = {
78 | set("inclusive_end", inclusiveEnd)
79 | }
80 |
81 | def key[D: W](key: D): ViewQueryBuilder[K, V, DR, MR] = {
82 | set("key", write(key))
83 | }
84 |
85 | def limit(limit: Int): ViewQueryBuilder[K, V, DR, MR] = {
86 | set("limit", limit)
87 | }
88 |
89 | def reduce[A: R]: ViewQueryBuilder[K, V, DR, MapWithReduce[A]] = {
90 | set("reduce", true)
91 | }
92 |
93 | def noReduce: ViewQueryBuilder[K, V, DR, MapOnly] = {
94 | set("reduce", false)
95 | }
96 |
97 | def withIds(ids: Seq[K]): ViewQueryBuilder[K, V, DR, MR] = {
98 | set(params, ids)
99 | }
100 |
101 | def skip(skip: Int): ViewQueryBuilder[K, V, DR, MR] = {
102 | set("skip", skip)
103 | }
104 |
105 | def stale(stale: String): ViewQueryBuilder[K, V, DR, MR] = {
106 | set("stale", stale)
107 | }
108 |
109 | def startKey[K2: W](startKey: K2): ViewQueryBuilder[K, V, DR, MR] = {
110 | set("startkey", write(startKey))
111 | }
112 |
113 | def startKeyDocId(startKeyDocId: String): ViewQueryBuilder[K, V, DR, MR] = {
114 | set("startkey_docid", startKeyDocId)
115 | }
116 |
117 | def updateSeq(updateSeq: Boolean = true): ViewQueryBuilder[K, V, DR, MR] = {
118 | set("update_seq", updateSeq)
119 | }
120 |
121 | private def set[I <: DocsInResult, M <: ViewOperation](key: String, value: String):
122 | ViewQueryBuilder[K, V, I, M] = {
123 | set(params.updated(key, value), ids)
124 | }
125 |
126 | private def set[I <: DocsInResult, M <: ViewOperation](key: String, value: Any):
127 | ViewQueryBuilder[K, V, I, M] = {
128 | set(key, value.toString)
129 | }
130 |
131 | private def set[I <: DocsInResult, M <: ViewOperation](
132 | _params: Map[String, String],
133 | _ids: Seq[K]): ViewQueryBuilder[K, V, I, M] = {
134 | new ViewQueryBuilder(client, db, design, view, temporaryView, _params, _ids)
135 | }
136 | }
137 |
138 | object ViewQueryBuilder {
139 |
140 | private type VBuilder[K, V, ID <: DocsInResult, MR <: ViewOperation] =
141 | ViewQueryBuilder[K, V, ID, MR]
142 |
143 | case class Builder[K: R, V: R, T: R, ID <: DocsInResult, MR <: ViewOperation](
144 | builder: VBuilder[K, V, ID, MR])(implicit kr: W[K], cdr: R[CouchKeyVals[K, V]]) {
145 | def build: QueryView[K, T] = QueryView(
146 | builder.client, builder.db, builder.design,
147 | builder.params, builder.ids, builder.view, builder.temporaryView)
148 | }
149 |
150 | private type BasicBuilder[K, V] = Builder[K, V, CouchKeyVals[K, V], ExcludeDocs, MapOnly]
151 |
152 | private type MapWithReduceBuilder[K, V, A] = Builder[K, V, CouchReducedKeyVals[K, A],
153 | ExcludeDocs, MapWithReduce[A]]
154 |
155 | private type IncludeDocsBuilder[K, V, D] = Builder[K, V, CouchDocs[K, V, D], IncludeDocs[D],
156 | MapOnly]
157 |
158 | implicit def buildBasic[K: R, V: R](
159 | builder: VBuilder[K, V, ExcludeDocs, MapOnly])(implicit kw: W[K]):
160 | Builder[K, V, CouchKeyVals[K, V], ExcludeDocs, MapOnly] =
161 | new BasicBuilder(builder)
162 |
163 | implicit def buildReduced[K: R, V: R, A: R](
164 | builder: VBuilder[K, V, ExcludeDocs, MapWithReduce[A]])(implicit kw: W[K]):
165 | Builder[K, V, CouchReducedKeyVals[K, A], ExcludeDocs, MapWithReduce[A]] = {
166 | new MapWithReduceBuilder(if (builder.ids.isEmpty) builder else builder.group())
167 | }
168 |
169 | implicit def includeDocsBuilder[K: R, V: R, D: R](
170 | builder: VBuilder[K, V, IncludeDocs[D], MapOnly])(implicit kw: W[K]):
171 | Builder[K, V, CouchDocs[K, V, D], IncludeDocs[D], MapOnly] = new IncludeDocsBuilder(builder)
172 |
173 | def apply[K: R, V: R](client: Client, db: String, design: String, view: String)
174 | (implicit kw: W[K]): ViewQueryBuilder[K, V, ExcludeDocs, MapOnly] = {
175 | new ViewQueryBuilder(client, db, design = Option(design), view = Option(view))
176 | }
177 |
178 | def apply[K: R, V: R](client: Client, db: String, view: CouchView)(implicit kw: W[K]):
179 | ViewQueryBuilder[K, V, ExcludeDocs, MapOnly] = {
180 | new ViewQueryBuilder(client, db, design = None, view = None, temporaryView = Option(view))
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/core/Client.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation, Google Inc.
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 |
17 | package com.ibm.couchdb.core
18 |
19 | import com.ibm.couchdb._
20 | import org.http4s.Http4s._
21 | import org.http4s.Method._
22 | import org.http4s._
23 | import org.http4s.client.blaze.PooledHttp1Client
24 | import org.http4s.headers.Authorization
25 | import org.http4s.util.CaseInsensitiveString
26 | import scodec.bits.ByteVector
27 | import upickle.default.Aliases.{R, W}
28 | import upickle.default.{read, write}
29 |
30 | import scalaz.Scalaz._
31 | import scalaz.concurrent.Task
32 |
33 | class Client(config: Config) {
34 |
35 | private val log = org.log4s.getLogger
36 |
37 | val client = PooledHttp1Client()
38 |
39 | val baseHeaders = config.credentials match {
40 | case Some(x) => Headers(Authorization(BasicCredentials(x._1, x._2)))
41 | case None => Headers()
42 | }
43 |
44 | val baseHeadersWithAccept = baseHeaders.put(Header("Accept", "application/json; charset=utf-8"))
45 |
46 | val baseUri = Uri(
47 | scheme = CaseInsensitiveString(if (config.https) "https" else "http").some,
48 | authority = Uri.Authority(
49 | host = Uri.IPv4(address = config.host),
50 | port = config.port.some
51 | ).some)
52 |
53 | def url(resource: String, params: Seq[(String, String)] = Seq.empty[(String, String)]): Uri = {
54 | baseUri.copy(path = resource).setQueryParams(
55 | params.map(x => (x._1, Seq(x._2))).toMap)
56 | }
57 |
58 | def req(request: Request, expectedStatus: Status): Task[Response] = {
59 | log.debug(s"Making a request $request")
60 | client.toHttpService.run(request) flatMap { response =>
61 | log.debug(s"Received response $response")
62 | if (response.status == expectedStatus) {
63 | Task.now(response)
64 | } else {
65 | log.warn(s"Unexpected response status ${response.status}, expected $expectedStatus")
66 | for {
67 | responseBody <- response.as[String]
68 | requestBody <- EntityDecoder.decodeString(request)
69 | errorRaw = read[Res.Error](responseBody)
70 | error = errorRaw.copy(
71 | status = response.status,
72 | request = request.toString,
73 | requestBody = requestBody
74 | )
75 | _ = log.warn(s"Request error $error")
76 | fail <- Task.fail(CouchException[Res.Error](error))
77 | } yield fail
78 | }
79 | }
80 | }
81 |
82 | def reqAndRead[T: R](request: Request, expectedStatus: Status): Task[T] = {
83 | for {
84 | response <- req(request, expectedStatus)
85 | asString <- response.as[String]
86 | } yield read[T](asString)
87 | }
88 |
89 | def getRaw(
90 | resource: String,
91 | expectedStatus: Status,
92 | params: Seq[(String, String)] = Seq.empty[(String, String)]): Task[String] = {
93 | val request = Request(
94 | method = GET,
95 | uri = url(resource, params),
96 | headers = baseHeadersWithAccept)
97 | req(request, expectedStatus).as[String]
98 | }
99 |
100 | def get[T: R](
101 | resource: String,
102 | expectedStatus: Status,
103 | params: Seq[(String, String)] = Seq.empty[(String, String)]): Task[T] = {
104 | val request = Request(
105 | method = GET,
106 | uri = url(resource, params),
107 | headers = baseHeadersWithAccept)
108 | reqAndRead[T](request, expectedStatus)
109 | }
110 |
111 | def getBinary(resource: String, expectedStatus: Status): Task[Array[Byte]] = {
112 | val request = Request(
113 | method = GET,
114 | uri = url(resource))
115 | req(request, expectedStatus).as[ByteVector].map(_.toArray)
116 | }
117 |
118 | private def put[T: R](
119 | resource: String,
120 | expectedStatus: Status,
121 | entity: EntityEncoder.Entity,
122 | contentType: String): Task[T] = {
123 | val headers =
124 | if (!contentType.isEmpty) baseHeadersWithAccept.put(Header("Content-Type", contentType))
125 | else baseHeadersWithAccept
126 | val request = Request(
127 | method = PUT,
128 | uri = url(resource),
129 | headers = headers,
130 | body = entity.body)
131 | reqAndRead[T](request, expectedStatus)
132 | }
133 |
134 | def put[B: W, T: R](
135 | resource: String,
136 | expectedStatus: Status,
137 | body: B): Task[T] = {
138 | EntityEncoder[String].toEntity(write(body)) flatMap { entity =>
139 | put[T](resource, expectedStatus, entity, "")
140 | }
141 | }
142 |
143 | def put[T: R](
144 | resource: String,
145 | expectedStatus: Status,
146 | body: Array[Byte] = Array(),
147 | contentType: String = ""): Task[T] = {
148 | EntityEncoder[Array[Byte]].toEntity(body) flatMap { entity =>
149 | put[T](resource, expectedStatus, entity, contentType)
150 | }
151 | }
152 |
153 | def post[B: W, T: R](
154 | resource: String,
155 | expectedStatus: Status,
156 | body: B,
157 | params: Seq[(String, String)] = Seq.empty[(String, String)]): Task[T] = {
158 | EntityEncoder[String].toEntity(write(body)) flatMap { entity =>
159 | val request = Request(
160 | method = POST,
161 | uri = url(resource, params),
162 | headers = baseHeadersWithAccept.put(
163 | Header("Content-Type", "application/json")),
164 | body = entity.body)
165 | reqAndRead[T](request, expectedStatus)
166 | }
167 | }
168 |
169 | def delete[T: R](resource: String, expectedStatus: Status): Task[T] = {
170 | val request = Request(
171 | method = DELETE,
172 | uri = url(resource),
173 | headers = baseHeadersWithAccept)
174 | reqAndRead[T](request, expectedStatus)
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/implicits/TaskImplicits.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.implicits
18 |
19 | import com.ibm.couchdb.Res
20 |
21 | import scalaz.concurrent.Task
22 |
23 | trait TaskImplicits {
24 |
25 | implicit class TaskOps[T](task: Task[T]) {
26 | def ignoreError: Task[Res.Ok] = {
27 | task.attempt.map(_ => Res.Ok())
28 | }
29 | }
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/implicits/UpickleImplicits.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.implicits
18 |
19 | import com.ibm.couchdb._
20 | import org.http4s.Status
21 | import upickle.Js.Value
22 | import upickle.default.Aliases.{R, W}
23 | import upickle.default.{Reader => Rr, Writer => Wr, readJs => rJs, writeJs => wJs}
24 | import upickle.{Js, Types}
25 |
26 | import scala.util.Try
27 | import scalaz.{-\/, \/, \/-}
28 |
29 | trait UpickleImplicits extends Types {
30 |
31 | implicit val statusW: W[Status] = Wr[Status] {
32 | x => Js.Num(x.code.toDouble)
33 | }
34 |
35 | implicit val statusR: R[Status] = Rr[Status] {
36 | case json: Js.Num => Status.fromInt(json.value.toInt).toOption.get
37 | }
38 |
39 | implicit def dockViewWithKeysW[K: W]: W[Req.ViewWithKeys[K]] = Wr[Req.ViewWithKeys[K]] {
40 | case Req.ViewWithKeys(keys, CouchView(map, reduce)) =>
41 | Js.Obj(mapReduceParams(map, reduce) ++ Seq("keys" -> wJs(keys)): _*)
42 | }
43 |
44 | implicit val couchViewW: W[CouchView] = Wr[CouchView] {
45 | case CouchView(map, reduce) => Js.Obj(mapReduceParams(map, reduce): _*)
46 | }
47 |
48 | private def mapReduceParams(map: String, reduce: String = ""): Seq[(String, Value)] = {
49 | val m = Seq("map" -> wJs(map))
50 | if (reduce.isEmpty) m else m ++ Seq("reduce" -> wJs(reduce))
51 | }
52 |
53 | implicit def couchKeyValOrErrorR[K: R, V: R]: Rr[\/[CouchKeyError[K], CouchKeyVal[K, V]]] = Rr {
54 | case o: Js.Obj => Try(\/-(rJs[CouchKeyVal[K, V]](o))).getOrElse(-\/(rJs[CouchKeyError[K]](o)))
55 | }
56 |
57 | implicit def couchKeyValDocOrErrorR[K: R, V: R, D: R]: Rr[\/[CouchKeyError[K],
58 | CouchKeyValWithDoc[K, V, D]]] = Rr {
59 | case o: Js.Obj => Try(\/-(rJs[CouchKeyValWithDoc[K, V, D]](o))).getOrElse(
60 | -\/(rJs[CouchKeyError[K]](o)))
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/scala/com/ibm/couchdb/package.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm
18 |
19 | import com.ibm.couchdb.implicits.{TaskImplicits, UpickleImplicits}
20 |
21 | package object couchdb extends TaskImplicits with UpickleImplicits {}
22 |
--------------------------------------------------------------------------------
/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/test/scala/com/ibm/couchdb/BasicAuthSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb
18 |
19 | import com.ibm.couchdb.spec.{CouchDbSpecification, SpecConfig}
20 | import org.http4s.Status
21 |
22 | class BasicAuthSpec extends CouchDbSpecification {
23 |
24 | val couch = CouchDb(SpecConfig.couchDbHost, SpecConfig.couchDbPort)
25 | val couchAdmin = CouchDb(
26 | SpecConfig.couchDbHost,
27 | SpecConfig.couchDbPort,
28 | https = false,
29 | SpecConfig.couchDbUsername,
30 | SpecConfig.couchDbPassword)
31 |
32 | val db = "couchdb-scala-basic-auth-spec"
33 | val adminUrl = s"/_config/admins/${SpecConfig.couchDbUsername}"
34 |
35 | "Basic authentication" >> {
36 |
37 | "Only admin can create and delete databases" >> {
38 | awaitRight(
39 | couch.client.put[String, String](
40 | adminUrl, Status.Ok, SpecConfig.couchDbPassword)) mustEqual ""
41 | awaitError(couch.dbs.create(db), "unauthorized")
42 | await(couchAdmin.dbs.delete(db))
43 | awaitOk(couchAdmin.dbs.create(db))
44 | awaitDocOk(couch.db(db, typeMapping).docs.create(fixAlice))
45 | awaitDocOk(couchAdmin.db(db, typeMapping).docs.create(fixAlice))
46 | awaitRight(couchAdmin.client.delete[String](adminUrl, Status.Ok)) must not be empty
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/test/scala/com/ibm/couchdb/CouchDbSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb
18 |
19 | import com.ibm.couchdb.api.{Design, Documents}
20 | import com.ibm.couchdb.spec.{CouchDbSpecification, SpecConfig}
21 | import org.http4s.Status
22 | import org.specs2.matcher.MatchResult
23 |
24 | class CouchDbSpec extends CouchDbSpecification {
25 |
26 | val couch = CouchDb(SpecConfig.couchDbHost, SpecConfig.couchDbPort)
27 |
28 | val db1 = "couchdb-scala-couchdb-spec1"
29 | val db2 = "couchdb-scala-couchdb-spec2"
30 |
31 | "User interface" >> {
32 |
33 | "Get info about the DB instance" >> {
34 | awaitRight(couch.server.info).couchdb mustEqual "Welcome"
35 | }
36 |
37 | "Create and query 2 DBs" >> {
38 | def testDb(dbName: String): MatchResult[Seq[CouchKeyVal[String, String]]] = {
39 | await(couch.dbs.delete(dbName))
40 | val error = awaitLeft(couch.dbs.delete(dbName))
41 | error.error mustEqual "not_found"
42 | error.reason mustEqual "missing"
43 | error.status mustEqual Status.NotFound
44 | error.request must contain("DELETE")
45 | error.request must contain(dbName)
46 | error.requestBody must beEmpty
47 |
48 | awaitOk(couch.dbs.create(dbName))
49 | couch.db(dbName, typeMapping).name mustEqual dbName
50 | couch.db(dbName, typeMapping).docs must beAnInstanceOf[Documents]
51 | couch.db(dbName, typeMapping).design must beAnInstanceOf[Design]
52 |
53 | val db = couch.db(dbName, typeMapping)
54 | db must beTheSameAs(couch.db(dbName, typeMapping))
55 | awaitDocOk(db.docs.create(fixAlice))
56 | awaitDocOk(db.design.create(fixDesign))
57 |
58 | val docs = awaitRight(
59 | db.query.view[String, String](fixDesign.name, FixViews.names).get.build.query)
60 | docs.rows must haveLength(1)
61 | }
62 | testDb(db1)
63 | testDb(db2)
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/test/scala/com/ibm/couchdb/api/DatabasesSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.api
18 |
19 | import com.ibm.couchdb.spec.CouchDbSpecification
20 |
21 | class DatabasesSpec extends CouchDbSpecification {
22 |
23 | val db = "couchdb-scala-databases-spec"
24 | val databases = new Databases(client)
25 |
26 | private def clear() = await(databases.delete(db))
27 |
28 | "Databases API" >> {
29 |
30 | "Create a DB" >> {
31 | clear()
32 | awaitOk(databases.create(db))
33 | awaitError(databases.create(db), "file_exists")
34 | }
35 |
36 | "Get a DB" >> {
37 | clear()
38 | awaitOk(databases.create(db))
39 | val info = awaitRight(databases.get(db))
40 | info.db_name mustEqual db
41 | info.doc_count mustEqual 0
42 | info.doc_del_count mustEqual 0
43 | }
44 |
45 | "Get all DBs" >> {
46 | awaitRight(databases.getAll)
47 | await(databases.create(db))
48 | awaitRight(databases.getAll) must contain(db)
49 | }
50 |
51 | "Delete a DB" >> {
52 | clear()
53 | awaitError(databases.delete(db), "not_found")
54 | awaitOk(databases.create(db))
55 | awaitOk(databases.delete(db))
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/test/scala/com/ibm/couchdb/api/DesignSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.api
18 |
19 | import com.ibm.couchdb.spec.CouchDbSpecification
20 |
21 | class DesignSpec extends CouchDbSpecification {
22 |
23 | val db = "couchdb-scala-design-spec"
24 | val databases = new Databases(client)
25 | val design = new Design(client, db)
26 | val documents = new Documents(client, db, typeMapping)
27 |
28 | private def clear() = recreateDb(databases, db)
29 |
30 | "Design API" >> {
31 |
32 | "Create a design document" >> {
33 | clear()
34 | awaitDocOk(design.create(fixDesign), s"_design/${fixDesign.name}")
35 | }
36 |
37 | "Get info about a design document" >> {
38 | clear()
39 | awaitDocOk(design.create(fixDesign))
40 | val info = awaitRight(design.info(fixDesign.name))
41 | info.name mustEqual fixDesign.name
42 | info.view_index.language mustEqual "javascript"
43 | info.view_index.disk_size must beGreaterThan(0)
44 | }
45 |
46 | "Get a design document" >> {
47 | clear()
48 | awaitDocOk(design.create(fixDesign))
49 | val doc = awaitRight(design.get(fixDesign.name))
50 | doc._id mustEqual s"_design/${fixDesign.name}"
51 | doc._rev must beRev
52 | doc.views must haveKey("names")
53 | }
54 |
55 | "Get a design document by ID" >> {
56 | clear()
57 | val created = awaitRight(design.create(fixDesign))
58 | val doc = awaitRight(design.getById(created.id))
59 | doc._id mustEqual s"_design/${fixDesign.name}"
60 | doc._rev must beRev
61 | doc.views must haveKey("names")
62 | }
63 |
64 | "Update a design document" >> {
65 | clear()
66 | awaitDocOk(design.create(fixDesign))
67 | val initial = awaitRight(design.get(fixDesign.name))
68 | val docOk = awaitRight(design.update(initial.copy(language = "typescript")))
69 | checkDocOk(docOk, s"_design/${fixDesign.name}")
70 | val updated = awaitRight(design.get(fixDesign.name))
71 | updated._rev mustEqual docOk.rev
72 | updated._id mustEqual initial._id
73 | updated.views mustEqual initial.views
74 | updated.language mustEqual "typescript"
75 | }
76 |
77 | "Delete a design document" >> {
78 | clear()
79 | awaitDocOk(design.create(fixDesign))
80 | val doc = awaitRight(design.get(fixDesign.name))
81 | awaitDocOk(design.delete(doc), s"_design/${fixDesign.name}")
82 | }
83 |
84 | "Delete a design document by name" >> {
85 | clear()
86 | awaitDocOk(design.create(fixDesign))
87 | awaitDocOk(design.deleteByName(fixDesign.name), s"_design/${fixDesign.name}")
88 | }
89 |
90 | "Attach a byte array to a design" >> {
91 | clear()
92 | awaitDocOk(design.create(fixDesign))
93 | val doc = awaitRight(design.get(fixDesign.name))
94 | awaitDocOk(design.attach(doc, fixAttachmentName, fixAttachmentData), doc._id)
95 | }
96 |
97 | "Get a byte array attachment" >> {
98 | clear()
99 | awaitDocOk(design.create(fixDesign))
100 | val doc = awaitRight(design.get(fixDesign.name))
101 | awaitDocOk(design.attach(doc, fixAttachmentName, fixAttachmentData), doc._id)
102 | awaitRight(design.getAttachment(doc, fixAttachmentName)) mustEqual fixAttachmentData
103 | }
104 |
105 | "Get a design with attachment stubs" >> {
106 | clear()
107 | awaitDocOk(design.create(fixDesign))
108 | val designDoc = awaitRight(design.get(fixDesign.name))
109 | awaitDocOk(
110 | design.attach(designDoc, fixAttachmentName, fixAttachmentData, fixAttachmentContentType),
111 | designDoc._id)
112 | val doc = awaitRight(design.get(fixDesign.name))
113 | doc._id mustEqual designDoc._id
114 | doc._attachments must haveLength(1)
115 | doc._attachments must haveKey(fixAttachmentName)
116 | val meta = doc._attachments(fixAttachmentName)
117 | meta.content_type mustEqual fixAttachmentContentType
118 | meta.length mustEqual fixAttachmentData.length
119 | meta.stub mustEqual true
120 | meta.digest must not be empty
121 | }
122 |
123 | "Get a design with attachments inline" >> {
124 | clear()
125 | awaitDocOk(design.create(fixDesign))
126 | val designDoc = awaitRight(design.get(fixDesign.name))
127 | awaitDocOk(
128 | design.attach(designDoc, fixAttachmentName, fixAttachmentData, fixAttachmentContentType),
129 | designDoc._id)
130 | val doc = awaitRight(design.getWithAttachments(fixDesign.name))
131 | doc._id mustEqual designDoc._id
132 | doc._attachments must haveLength(1)
133 | doc._attachments must haveKey(fixAttachmentName)
134 | val attachment = doc._attachments(fixAttachmentName)
135 | attachment.content_type mustEqual fixAttachmentContentType
136 | attachment.length mustEqual -1
137 | attachment.stub mustEqual false
138 | attachment.digest must not be empty
139 | attachment.toBytes mustEqual fixAttachmentData
140 | }
141 |
142 | "Delete an attachment to a design" >> {
143 | clear()
144 | awaitDocOk(design.create(fixDesign))
145 | val doc = awaitRight(design.get(fixDesign.name))
146 | val attachment = awaitRight(design.attach(doc, fixAttachmentName, fixAttachmentData))
147 | val docWithAttachment = awaitRight(design.get(fixDesign.name))
148 | awaitDocOk(design.deleteAttachment(docWithAttachment, fixAttachmentName), attachment.id)
149 | awaitError(design.getAttachment(doc, fixAttachmentName), "not_found")
150 | }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/test/scala/com/ibm/couchdb/api/DocumentsSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation, Google Inc.
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 |
17 | package com.ibm.couchdb.api
18 |
19 | import com.ibm.couchdb.Res.DocOk
20 | import com.ibm.couchdb._
21 | import com.ibm.couchdb.spec.{CouchDbSpecification, SpecConfig}
22 | import org.http4s.Status
23 | import org.specs2.matcher.MatchResult
24 |
25 | class DocumentsSpec extends CouchDbSpecification {
26 |
27 | val db = "couchdb-scala-documents-spec"
28 | val server = new Server(client)
29 | val databases = new Databases(client)
30 | val documents = new Documents(client, db, typeMapping)
31 |
32 | private def clear() = recreateDb(databases, db)
33 |
34 | clear()
35 |
36 | "Documents API" >> {
37 |
38 | "Create a document with an auto-generated UUID" >> {
39 | awaitDocOk(documents.create(fixAlice))
40 | }
41 |
42 | "Create a document with a specified UUID" >> {
43 | val uuid = awaitRight(server.mkUuid)
44 | awaitDocOk(documents.create(fixAlice, uuid), uuid)
45 | }
46 |
47 | "Get a document by a UUID" >> {
48 | val uuid = awaitRight(server.mkUuid)
49 | awaitRight(documents.create(fixAlice, uuid))
50 | awaitRight(documents.get[FixPerson](uuid)).doc mustEqual fixAlice
51 | val aliceRes = awaitRight(documents.get[FixPerson](uuid))
52 | aliceRes.doc.name mustEqual fixAlice.name
53 | aliceRes.doc.age mustEqual fixAlice.age
54 | aliceRes._id mustEqual uuid
55 | aliceRes._rev must beRev
56 | }
57 |
58 | "Get a document by a non-existent UUID" >> {
59 | val uuid = awaitRight(server.mkUuid)
60 | awaitError(documents.get[FixPerson](uuid), "not_found")
61 | awaitError(documents.get[FixPerson](""), "not_found")
62 | }
63 |
64 | "Create multiple documents in bulk" >> {
65 | clear()
66 | val res = awaitRight(documents.createMany(Seq(fixAlice, fixBob)))
67 | res must haveLength(2)
68 | checkDocOk(res.head)
69 | checkDocOk(res(1))
70 | }
71 |
72 | "Create multiple documents in bulk with IDs" >> {
73 | clear()
74 | val docs = Map("1" -> fixAlice, "2" -> fixBob)
75 | val res = awaitRight(documents.createMany(docs))
76 | res must haveLength(docs.size)
77 | res.foreach(checkDocOk)
78 | res.map(_.id) mustEqual docs.keys.toList
79 | val created = awaitRight(documents.getMany[FixPerson](docs.keys.toList))
80 | created.getDocs.map(_.doc) mustEqual docs.values.toSeq
81 | created.rows.map(_.id) mustEqual docs.keys.toSeq
82 | }
83 |
84 | "Get all documents" >> {
85 | def verify(docs: CouchKeyVals[String, CouchDocRev], expected: Seq[Res.DocOk]):
86 | MatchResult[Seq[String]] = {
87 | docs.offset mustEqual 0
88 | docs.total_rows must beEqualTo(expected.size)
89 | docs.rows must haveLength(expected.size)
90 | docs.rows.map(_.id) must containTheSameElementsAs(expected.map(_.id))
91 | }
92 | clear()
93 | val created = Seq(
94 | awaitRight(documents.create(fixAlice)), awaitRight(documents.create(fixAlice)))
95 | val docs = awaitRight(documents.getMany.build.query)
96 | val docsWithExcludeDocs = awaitRight(documents.getMany.excludeDocs.build.query)
97 | verify(docs, created)
98 | verify(docsWithExcludeDocs, created)
99 | }
100 |
101 | "Get multiple documents by IDs" >> {
102 | def verify(
103 | docs: CouchKeyVals[String, CouchDocRev], created: Seq[Res.DocOk],
104 | expected: Seq[Res.DocOk]): MatchResult[Any] = {
105 | docs.offset mustEqual 0
106 | docs.total_rows must beEqualTo(created.size)
107 | docs.rows must haveLength(expected.size)
108 | docs.rows.map(_.id) mustEqual expected.map(_.id)
109 | }
110 | clear()
111 | val created = Seq(fixAlice, fixCarl, fixBob).map(x => awaitRight(documents.create(x)))
112 | val expected = created.take(2)
113 | verify(
114 | awaitRight(documents.getMany.withIds(expected.map(_.id)).build.query), created, expected)
115 | verify(
116 | awaitRight(
117 | documents.getMany.disallowMissing.excludeDocs.withIds(expected.map(_.id)).build.query),
118 | created, expected)
119 | }
120 |
121 | "Get multiple documents by IDs with some missing" >> {
122 | def verify(
123 | docs: CouchKeyValsIncludesMissing[String, CouchDocRev], missing: Seq[String],
124 | existing: Seq[String]): MatchResult[Any] = {
125 | docs.offset mustEqual 0
126 | docs.rows must haveLength(missing.length + existing.length)
127 | docs.rows.flatMap(_.toOption).map(_.id).toList mustEqual existing
128 | docs.rows.flatMap(_.swap.toOption).map(_.key).toList mustEqual missing
129 | }
130 | clear()
131 | val fixPersons = Seq(fixAlice, fixBob, fixCarl)
132 | val createdPersons = fixPersons.map(person => awaitRight(documents.create(person)))
133 | val missingIds = Seq("non-existent-id-1", "non-existent-id-2")
134 | val existingIds = createdPersons.map(_.id)
135 | verify(
136 | awaitRight(documents.getMany.allowMissing.withIds(existingIds ++ missingIds).build.query),
137 | missingIds, existingIds)
138 | verify(
139 | awaitRight(
140 | documents.getMany.disallowMissing.allowMissing.
141 | withIds(existingIds ++ missingIds).build.query), missingIds, existingIds)
142 | }
143 |
144 | "Get all documents and include the doc data" >> {
145 | def verify(
146 | docs: CouchDocs[String, CouchDocRev, FixPerson],
147 | created: Seq[Res.DocOk], expected: Seq[FixPerson]): MatchResult[Any] = {
148 | docs.offset mustEqual 0
149 | docs.total_rows mustEqual created.size
150 | docs.rows must haveLength(expected.size)
151 | docs.rows.map(_.id) mustEqual created.map(_.id)
152 | docs.rows.map(_.doc.doc) mustEqual expected
153 | docs.getDocs.map(_.doc) mustEqual expected
154 | docs.getDocsData mustEqual expected
155 | }
156 | clear()
157 | val expected = Seq(fixAlice, fixBob)
158 | val created = expected.map(x => awaitRight(documents.create(x)))
159 | verify(awaitRight(documents.getMany.includeDocs[FixPerson].build.query), created, expected)
160 | verify(
161 | awaitRight(documents.getMany.excludeDocs.includeDocs[FixPerson].build.query), created,
162 | expected)
163 | }
164 |
165 | "Get all documents by type" >> {
166 | def verify(
167 | docs: CouchKeyVals[(String, String), String], created: Seq[DocOk],
168 | expected: Seq[FixXPerson]): MatchResult[Any] = {
169 | docs.total_rows must beGreaterThanOrEqualTo(created.size)
170 | docs.rows must haveLength(expected.size)
171 | docs.rows.map(_.value) mustEqual created.map(_.id)
172 | }
173 | clear()
174 | awaitRight(documents.createMany(Seq(fixAlice, fixBob)))
175 | val expected = Seq(fixProfessorX, fixMagneto)
176 | val createdXMenOnly = awaitRight(documents.createMany(expected))
177 | verify(
178 | awaitRight(
179 | documents.getMany.byTypeUsingTemporaryView(
180 | typeMapping.get(
181 | classOf[FixXPerson]).get).build.query), createdXMenOnly, expected)
182 | }
183 |
184 | "Get all documents by type and include the doc data" >> {
185 | def verify(
186 | docs: CouchDocs[(String, String), String, FixXPerson],
187 | created: Seq[DocOk], expected: Seq[FixXPerson]): MatchResult[Any] = {
188 | docs.total_rows must beGreaterThanOrEqualTo(created.size)
189 | docs.rows must haveLength(expected.size)
190 | docs.rows.map(_.value) mustEqual created.map(_.id)
191 | docs.rows.map(_.doc.doc) mustEqual expected
192 | docs.getDocs.map(_.doc) mustEqual expected
193 | docs.getDocsData mustEqual expected
194 | }
195 | clear()
196 | awaitRight(documents.createMany(Seq(fixAlice, fixBob)))
197 | val expected = Seq(fixProfessorX, fixMagneto)
198 | val createdXMenOnly = awaitRight(documents.createMany(expected))
199 | verify(
200 | awaitRight(
201 | documents.getMany.includeDocs[FixXPerson].byTypeUsingTemporaryView(
202 | typeMapping.get(classOf[FixXPerson]).get).build.query), createdXMenOnly, expected)
203 | }
204 |
205 | "Get all documents by type given a permanent type filter view" >> {
206 | def verify(
207 | docs: CouchKeyVals[_, String], created: Seq[DocOk],
208 | expected: Seq[FixXPerson]): MatchResult[Any] = {
209 | docs.total_rows must beGreaterThan(created.size)
210 | docs.rows must haveLength(expected.size)
211 | docs.rows.map(_.value) mustEqual created.map(_.id)
212 | }
213 | clear()
214 | val design = new Design(client, db)
215 | awaitRight(design.create(fixDesign))
216 | awaitRight(documents.createMany(Seq(fixAlice, fixBob)))
217 | val expected = Seq(fixProfessorX, fixMagneto)
218 | val created = awaitRight(documents.createMany(expected))
219 | val docs = awaitRight(
220 | documents.getMany.byType[String](
221 | FixViews.typeFilter, fixDesign.name,
222 | typeMapping.get(classOf[FixXPerson]).get).build.query)
223 | verify(docs, created, expected)
224 | val docsCustom = awaitRight(
225 | documents.getMany.byType[(String, String, Int), String](
226 | FixViews.typeFilterCustom,
227 | fixDesign.name, typeMapping.get(classOf[FixXPerson]).get).build.query)
228 | verify(docsCustom, created, expected)
229 | }
230 |
231 | "Get all documents by type and include the doc data, given a permanent type filter view" >> {
232 | def verify(
233 | docs: CouchDocs[_, String, FixXPerson], created: Seq[DocOk],
234 | expected: Seq[FixXPerson]): MatchResult[Any] = {
235 | docs.total_rows must beGreaterThan(created.size)
236 | docs.rows must haveLength(expected.size)
237 | docs.rows.map(_.value) mustEqual created.map(_.id)
238 | docs.rows.map(_.doc.doc) mustEqual expected
239 | docs.getDocs.map(_.doc) mustEqual expected
240 | docs.getDocsData mustEqual expected
241 | }
242 | clear()
243 | val design = new Design(client, db)
244 | awaitRight(design.create(fixDesign))
245 | awaitRight(documents.createMany(Seq(fixAlice, fixBob)))
246 | val expected = Seq(fixProfessorX, fixMagneto)
247 | val created = awaitRight(documents.createMany(expected))
248 | val docs = awaitRight(
249 | documents.getMany.includeDocs[FixXPerson].byType[String](
250 | FixViews.typeFilter, fixDesign.name,
251 | typeMapping.get(classOf[FixXPerson]).get).build.query)
252 | verify(docs, created, expected)
253 | val docsCustom = awaitRight(
254 | documents.getMany.includeDocs[FixXPerson].byType[(String, String, Int), String](
255 | FixViews
256 | .typeFilterCustom, fixDesign.name, typeMapping.get(classOf[FixXPerson]).get)
257 | .build.query)
258 | verify(docsCustom, created, expected)
259 |
260 | }
261 |
262 | "Get multiple documents by IDs and include the doc data" >> {
263 | def verify(
264 | docs: CouchDocs[String, CouchDocRev, FixPerson], created: Seq[Res.DocOk],
265 | expected: Seq[FixPerson]): MatchResult[Any] = {
266 | docs.offset mustEqual 0
267 | docs.total_rows must beGreaterThanOrEqualTo(3)
268 | docs.rows must haveLength(expected.size)
269 | docs.rows.map(_.id) mustEqual created.map(_.id)
270 | docs.rows.map(_.doc.doc) mustEqual expected
271 | docs.getDocs.map(_.doc) mustEqual expected
272 | docs.getDocsData mustEqual expected
273 | }
274 | clear()
275 | awaitRight(documents.create(fixBob))
276 | val expected = Seq(fixAlice, fixCarl)
277 | val created: Seq[Res.DocOk] = expected.map(x => awaitRight(documents.create(x)))
278 | verify(awaitRight(documents.getMany[FixPerson](created.map(_.id))), created, expected)
279 | verify(
280 | awaitRight(documents.getMany.withIds(created.map(_.id)).includeDocs[FixPerson].build.query),
281 | created, expected)
282 | verify(
283 | awaitRight(
284 | documents.getMany.disallowMissing.withIds(created.map(_.id)).includeDocs[FixPerson].
285 | build.query), created, expected)
286 | }
287 |
288 | "Get multiple documents by IDs with some missing and include the doc data" >> {
289 | def verify(
290 | docs: CouchDocsIncludesMissing[String, CouchDocRev, FixPerson],
291 | missing: Seq[String], existing: Seq[String],
292 | expected: Seq[FixPerson]): MatchResult[Any] = {
293 | docs.offset mustEqual 0
294 | docs.rows must haveLength(missing.length + existing.length)
295 | docs.rows.flatMap(_.toOption).map(_.id).toList mustEqual existing
296 | docs.rows.flatMap(_.toOption).map(_.doc.doc) mustEqual expected
297 | docs.getDocs.flatMap(_.toOption).map(_.doc) mustEqual expected
298 | docs.getDocsData mustEqual expected
299 | docs.rows.flatMap(_.swap.toOption).map(_.key).toList mustEqual missing
300 | }
301 | clear()
302 | val fixPersons = Seq(fixAlice, fixBob, fixCarl)
303 | val createdPersons = fixPersons.map(person => awaitRight(documents.create(person)))
304 | val missingIds = Seq("non-existent-id-1", "non-existent-id-2")
305 | val existingIds = createdPersons.map(_.id)
306 | val docs = awaitRight(
307 | documents.getMany.includeDocs[FixPerson].allowMissing.withIds(existingIds ++ missingIds).
308 | build.query)
309 | verify(docs, missingIds, existingIds, fixPersons)
310 | }
311 |
312 | "Get a document containing unicode values" >> {
313 | clear()
314 | val created = awaitRight(documents.create[FixPerson](fixHaile))
315 | awaitRight(documents.get[FixPerson](created.id)).doc mustEqual fixHaile
316 | }
317 |
318 | "Update a document" >> {
319 | val created = awaitRight(documents.create(fixAlice))
320 | val aliceRes = awaitRight(documents.get[FixPerson](created.id))
321 | val docOk = awaitRight(documents.update((_docPersonAge modify (_ + 1)) (aliceRes)))
322 | checkDocOk(docOk, aliceRes._id)
323 | val aliceRes2 = awaitRight(documents.get[FixPerson](aliceRes._id))
324 | aliceRes2._id mustEqual aliceRes._id
325 | aliceRes2._rev mustEqual docOk.rev
326 | aliceRes2.doc.name mustEqual fixAlice.name
327 | aliceRes2.doc.age mustEqual fixAlice.age + 1
328 | }
329 |
330 | "Fail to update a document without ID" >> {
331 | val created = awaitRight(documents.create(fixAlice))
332 | val aliceRes = awaitRight(documents.get[FixPerson](created.id))
333 | awaitError(documents.update(aliceRes.copy(_id = "")), "cannot_update")
334 | }
335 |
336 | "Fail to update a document with an outdated revision" >> {
337 | val created = awaitRight(documents.create(fixAlice))
338 | val aliceRes = awaitRight(documents.get[FixPerson](created.id))
339 | await(documents.update((_docPersonAge set 26) (aliceRes)))
340 | val error = awaitLeft(documents.update((_docPersonAge set 27) (aliceRes)))
341 | error.status mustEqual Status.Conflict
342 | error.error mustEqual "conflict"
343 | }
344 |
345 | "Delete a document" >> {
346 | val created = awaitRight(documents.create(fixAlice))
347 | val aliceRes = awaitRight(documents.get[FixPerson](created.id))
348 | awaitDocOk(documents.delete(aliceRes), aliceRes._id)
349 | awaitError(documents.get[FixPerson](aliceRes._id), "not_found")
350 | }
351 |
352 | "Attach a byte array to a document" >> {
353 | val created = awaitRight(documents.create(fixAlice))
354 | val aliceRes = awaitRight(documents.get[FixPerson](created.id))
355 | awaitDocOk(documents.attach(aliceRes, fixAttachmentName, fixAttachmentData), aliceRes._id)
356 | }
357 |
358 | "Get a byte array attachment" >> {
359 | val created = awaitRight(documents.create(fixAlice))
360 | val aliceRes = awaitRight(documents.get[FixPerson](created.id))
361 | awaitDocOk(documents.attach(aliceRes, fixAttachmentName, fixAttachmentData))
362 | val url = s"http://${SpecConfig.couchDbHost}:${SpecConfig.couchDbPort}" +
363 | s"/$db/${aliceRes._id}/$fixAttachmentName"
364 | awaitRight(documents.getAttachmentUrl(aliceRes, fixAttachmentName)) mustEqual url
365 | awaitRight(documents.getAttachment(aliceRes, fixAttachmentName)) mustEqual fixAttachmentData
366 | }
367 |
368 | "Get a document with attachment stubs" >> {
369 | val created = awaitRight(documents.create(fixAlice))
370 | val aliceRes = awaitRight(documents.get[FixPerson](created.id))
371 | awaitDocOk(
372 | documents.attach(aliceRes, fixAttachmentName, fixAttachmentData, fixAttachmentContentType),
373 | aliceRes._id)
374 | val doc = awaitRight(documents.get[FixPerson](aliceRes._id))
375 | doc._id mustEqual aliceRes._id
376 | doc.doc.name mustEqual aliceRes.doc.name
377 | doc._attachments must haveLength(1)
378 | doc._attachments must haveKey(fixAttachmentName)
379 | val meta = doc._attachments(fixAttachmentName)
380 | meta.content_type mustEqual fixAttachmentContentType
381 | meta.length mustEqual fixAttachmentData.length
382 | meta.stub mustEqual true
383 | meta.digest must not be empty
384 | }
385 |
386 | "Get a document with attachments inline" >> {
387 | val created = awaitRight(documents.create(fixAlice))
388 | val aliceRes = awaitRight(documents.get[FixPerson](created.id))
389 | awaitDocOk(
390 | documents.attach(aliceRes, fixAttachmentName, fixAttachmentData, fixAttachmentContentType),
391 | aliceRes._id)
392 | val doc = awaitRight(documents.get.attachments().query[FixPerson](aliceRes._id))
393 | doc._id mustEqual aliceRes._id
394 | doc.doc.name mustEqual aliceRes.doc.name
395 | doc._attachments must haveLength(1)
396 | doc._attachments must haveKey(fixAttachmentName)
397 | val attachment = doc._attachments(fixAttachmentName)
398 | attachment.content_type mustEqual fixAttachmentContentType
399 | attachment.length mustEqual -1
400 | attachment.stub mustEqual false
401 | attachment.digest must not be empty
402 | attachment.toBytes mustEqual fixAttachmentData
403 | }
404 |
405 | "Create and get a document with attachments" >> {
406 | val attachments = Map[String, CouchAttachment](
407 | fixAttachmentName -> CouchAttachment.fromBytes(
408 | fixAttachmentData, fixAttachmentContentType),
409 | fixAttachment2Name -> CouchAttachment.fromBytes(
410 | fixAttachment2Data, fixAttachment2ContentType))
411 | val created = awaitRight(documents.create(fixAlice, attachments))
412 | val doc = awaitRight(documents.get.attachments().query[FixPerson](created.id))
413 | doc._id mustEqual created.id
414 | doc._attachments must haveLength(2)
415 | doc._attachments must haveKeys(fixAttachmentName, fixAttachment2Name)
416 | val attachment = doc._attachments(fixAttachmentName)
417 | attachment.content_type mustEqual fixAttachmentContentType
418 | attachment.length mustEqual -1
419 | attachment.stub mustEqual false
420 | attachment.digest must not be empty
421 | attachment.toBytes mustEqual fixAttachmentData
422 | val attachment2 = doc._attachments(fixAttachment2Name)
423 | attachment2.content_type mustEqual fixAttachment2ContentType.toString
424 | attachment2.length mustEqual -1
425 | attachment2.stub mustEqual false
426 | attachment2.digest must not be empty
427 | attachment2.toBytes mustEqual fixAttachment2Data
428 | }
429 |
430 | "Update a document with attachments" >> {
431 | val created = awaitRight(documents.create(fixAlice))
432 | val aliceRes = awaitRight(documents.get[FixPerson](created.id))
433 | val aliceWithAttachments = aliceRes.copy(
434 | _attachments = Map(
435 | fixAttachmentName -> CouchAttachment.fromBytes(
436 | fixAttachmentData, fixAttachmentContentType),
437 | fixAttachment2Name -> CouchAttachment.fromBytes(
438 | fixAttachment2Data, fixAttachment2ContentType)))
439 | val docOk = awaitRight(documents.update(aliceWithAttachments))
440 | checkDocOk(docOk, aliceRes._id)
441 | val doc = awaitRight(documents.get.attachments().query[FixPerson](created.id))
442 | doc._id mustEqual created.id
443 | doc._attachments.keys must containTheSameElementsAs(
444 | Seq(fixAttachmentName, fixAttachment2Name))
445 | val attachment = doc._attachments(fixAttachmentName)
446 | attachment.content_type mustEqual fixAttachmentContentType
447 | attachment.length mustEqual -1
448 | attachment.stub mustEqual false
449 | attachment.digest must not be empty
450 | attachment.toBytes mustEqual fixAttachmentData
451 | val attachment2 = doc._attachments(fixAttachment2Name)
452 | attachment2.content_type mustEqual fixAttachment2ContentType.toString
453 | attachment2.length mustEqual -1
454 | attachment2.stub mustEqual false
455 | attachment2.digest must not be empty
456 | attachment2.toBytes mustEqual fixAttachment2Data
457 | }
458 |
459 | "Delete an attachment to a document" >> {
460 | clear()
461 | val created = awaitRight(documents.create(fixAlice))
462 | val aliceRes = awaitRight(documents.get[FixPerson](created.id))
463 | val attachment = awaitRight(documents.attach(aliceRes, fixAttachmentName, fixAttachmentData))
464 | val aliceWithAttachment = awaitRight(documents.get[FixPerson](created.id))
465 | awaitDocOk(documents.deleteAttachment(aliceWithAttachment, fixAttachmentName), attachment.id)
466 | awaitError(documents.getAttachment(aliceRes, fixAttachmentName), "not_found")
467 | }
468 |
469 | "Bulk update should" >> {
470 | val fixes = Seq(fixAlice, fixBob, fixHaile)
471 | def change(v: String): String = s"$v-updated"
472 | def create(x: Seq[FixPerson]): Seq[CouchDoc[FixPerson]] = {
473 | clear()
474 | val newIds = awaitRight(documents.createMany(x)).map(_.id)
475 | awaitRight(documents.getMany[FixPerson](newIds)).getDocs
476 | }
477 | def modify(orig: Seq[CouchDoc[FixPerson]]): Seq[CouchDoc[FixPerson]] =
478 | orig.map(x => (_docPersonName modify change) (x))
479 | def createAndModify: (Seq[FixPerson]) => Seq[CouchDoc[FixPerson]] = create _ andThen modify
480 |
481 | "update all documents when valid Ids and Rev" >> {
482 | val modified = createAndModify(fixes)
483 | val updatedDocs = awaitRight(documents.updateMany(modified)).map(_.id)
484 | updatedDocs.size mustEqual fixes.size
485 | val updateDocs = awaitRight(documents.getMany[FixPerson](modified.map(_._id)))
486 | updateDocs.getDocs.map(_.doc) mustEqual fixes.map(
487 | x => FixPerson(change(x.name), age = x.age))
488 | }
489 |
490 | "fail if one or more elements is missing Id" >> {
491 | val modified = createAndModify(fixes)
492 | val withInvalidId = modified.updated(2, modified(2).copy(_id = ""))
493 | awaitError(documents.updateMany(withInvalidId), "cannot_update")
494 | awaitRight(documents.getMany[FixPerson](modified.map(_._id)))
495 | .getDocs.map(_.doc) mustEqual fixes
496 | }
497 |
498 | "fail if one or more elements is missing Rev" >> {
499 | val modified = createAndModify(fixes)
500 | val withInvalidRev = modified.updated(1, modified(1).copy(_rev = ""))
501 | awaitError(documents.updateMany(withInvalidRev), "cannot_update")
502 | awaitRight(documents.getMany[FixPerson](modified.map(_._id)))
503 | .getDocs.map(_.doc) mustEqual fixes
504 | }
505 | }
506 |
507 | "Bulk delete should" >> {
508 | val fixes = Seq(fixAlice, fixBob, fixHaile)
509 | def create(x: Seq[FixPerson]): Seq[CouchDoc[FixPerson]] = {
510 | clear()
511 | val newIds = awaitRight(documents.createMany(x)).map(_.id)
512 | awaitRight(documents.getMany[FixPerson](newIds)).getDocs
513 | }
514 |
515 | "delete all documents" >> {
516 | val created = create(fixes)
517 | val deleted = awaitRight(documents.deleteMany(created)).map(_.id)
518 | deleted.size mustEqual fixes.size
519 | val getDeleted = awaitRight(documents.getMany[FixPerson](created.map(_._id)))
520 | getDeleted.getDocs.size mustEqual fixes.size
521 | getDeleted.getDocs.count(Option(_).isDefined) mustEqual 0
522 | }
523 |
524 | "fail if one or more elements is missing an Id" >> {
525 | val created = create(fixes)
526 | val withInvalidId = created.updated(1, created(1).copy(_id = ""))
527 | awaitError(documents.deleteMany(withInvalidId), "cannot_update")
528 | awaitRight(documents.getMany[FixPerson](created.map(_._id)))
529 | .getDocs.map(_.doc) mustEqual fixes
530 | }
531 |
532 | "fail if one or more elements is missing a Rev" >> {
533 | val created = create(fixes)
534 | val withInvalidRev = created.updated(2, created(2).copy(_id = ""))
535 | awaitError(documents.deleteMany(withInvalidRev), "cannot_update")
536 | awaitRight(documents.getMany[FixPerson](created.map(_._id)))
537 | .getDocs.map(_.doc) mustEqual fixes
538 | }
539 | }
540 | }
541 | }
542 |
--------------------------------------------------------------------------------
/src/test/scala/com/ibm/couchdb/api/QueryListSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.api
18 |
19 | import com.ibm.couchdb.spec.CouchDbSpecification
20 |
21 | class QueryListSpec extends CouchDbSpecification {
22 |
23 | val db = "couchdb-scala-query-list-spec"
24 | val databases = new Databases(client)
25 | val design = new Design(client, db)
26 | val documents = new Documents(client, db, typeMapping)
27 | val query = new Query(client, db)
28 | val list = query.list(fixDesign.name, FixLists.csvAll).get
29 |
30 | recreateDb(databases, db)
31 |
32 | val createdDesign = awaitRight(design.create(fixDesign))
33 | val createdAlice = awaitRight(documents.create(fixAlice))
34 | val createdBob = awaitRight(documents.create(fixBob))
35 | val createdCarl = awaitRight(documents.create(fixCarl))
36 |
37 | "Query List API" >> {
38 |
39 | "Query a list" >> {
40 | val expected = "Carl,20\nAlice,25\nBob,30\n"
41 | awaitRight(list.query(FixViews.compound)) mustEqual expected
42 | }
43 |
44 | "Query a list descending" >> {
45 | val expected = "Bob,30\nAlice,25\nCarl,20\n"
46 | awaitRight(list.descending().query(FixViews.compound)) mustEqual expected
47 | }
48 |
49 | "Query a list with a start key" >> {
50 | val expected = "Alice,25\nBob,30\n"
51 | awaitRight(list.startKey(Seq(21)).query(FixViews.compound)) mustEqual expected
52 | }
53 |
54 | "Query a list with custom params" >> {
55 | val expected = "name,age\nCarl,20\nAlice,25\nBob,30\n"
56 | awaitRight(list.addParam("header", "true").query(FixViews.compound)) mustEqual expected
57 | }
58 |
59 | "Query a list with a view from another design" >> {
60 | val expected = "Carl,20\nAlice,25\nBob,30\n"
61 | awaitRight(list.queryAnotherDesign(FixViews.compound, fixDesign.name)) mustEqual expected
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/test/scala/com/ibm/couchdb/api/QueryShowSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.api
18 |
19 | import com.ibm.couchdb.spec.CouchDbSpecification
20 |
21 | class QueryShowSpec extends CouchDbSpecification {
22 |
23 | val db = "couchdb-scala-query-show-spec"
24 | val databases = new Databases(client)
25 | val design = new Design(client, db)
26 | val documents = new Documents(client, db, typeMapping)
27 | val query = new Query(client, db)
28 | val show = query.show(fixDesign.name, FixShows.csv).get
29 |
30 | recreateDb(databases, db)
31 |
32 | val createdDesign = awaitRight(design.create(fixDesign))
33 | val createdAlice = awaitRight(documents.create(fixAlice))
34 | val createdBob = awaitRight(documents.create(fixBob))
35 | val createdCarl = awaitRight(documents.create(fixCarl))
36 |
37 | "Query Show API" >> {
38 |
39 | "Query a show without ID" >> {
40 | awaitRight(show.query) mustEqual "empty show"
41 | }
42 |
43 | "Query a show by ID" >> {
44 | awaitRight(show.query(createdAlice.id)) mustEqual s"${fixAlice.name},${fixAlice.age}"
45 | awaitRight(show.query(createdBob.id)) mustEqual s"${fixBob.name},${fixBob.age}"
46 | }
47 |
48 | "Query a show by ID with params" >> {
49 | val res = awaitRight(show.addParam("extra", "test").query(createdAlice.id))
50 | res mustEqual s"${fixAlice.name},${fixAlice.age},test"
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/test/scala/com/ibm/couchdb/api/QueryTemporaryViewSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 IBM Corporation
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 |
17 | package com.ibm.couchdb.api
18 |
19 | import com.ibm.couchdb.spec.CouchDbSpecification
20 |
21 | class QueryTemporaryViewSpec extends CouchDbSpecification {
22 |
23 | val db = "couchdb-scala-query-temp-view-spec"
24 | val databases = new Databases(client)
25 | val documents = new Documents(client, db, typeMapping)
26 | val query = new Query(client, db)
27 |
28 | val namesView = query.temporaryView[String, String](FixViews.namesView).get
29 | val compoundView = query.temporaryView[(Int, String), FixPerson](FixViews.compoundView).get
30 | val aggregateView = query.temporaryView[String, String](FixViews.reducedView).get
31 |
32 | recreateDb(databases, db)
33 |
34 | val createdAlice = awaitRight(documents.create(fixAlice))
35 | val createdBob = awaitRight(documents.create(fixBob))
36 | val createdCarl = awaitRight(documents.create(fixCarl))
37 |
38 | "Query Temporary View API" >> {
39 |
40 | "Query a temporary view" >> {
41 | val docs = awaitRight(namesView.build.query)
42 | docs.offset mustEqual 0
43 | docs.total_rows mustEqual 3
44 | docs.rows must haveLength(3)
45 | docs.rows.map(_.id) mustEqual Seq(createdAlice.id, createdBob.id, createdCarl.id)
46 | docs.rows.map(_.key) mustEqual Seq(fixAlice.name, fixBob.name, fixCarl.name)
47 | docs.rows.map(_.value) mustEqual Seq(fixAlice.name, fixBob.name, fixCarl.name)
48 | }
49 |
50 | "Query a temporary view with reducer" >> {
51 | val docs = awaitRight(aggregateView.reduce[Int].build.query)
52 | docs.rows must haveLength(1)
53 | docs.rows.head.value mustEqual Seq(fixCarl.age, fixBob.age, fixAlice.age).sum
54 | }
55 |
56 | "Query a temporary view with reducer given keys" >> {
57 | val docs = awaitRight(
58 | aggregateView.reduce[Int].withIds(Seq(createdCarl.id, createdAlice.id)).build.query)
59 | docs.rows must haveLength(2)
60 | docs.rows.map(_.value).sum mustEqual Seq(fixCarl.age, fixAlice.age).sum
61 | docs.rows.map(_.key) mustEqual Seq(createdCarl.id, createdAlice.id)
62 | }
63 |
64 | "Query a temporary view in the descending order" >> {
65 | val docs = awaitRight(namesView.descending().build.query)
66 | docs.offset mustEqual 0
67 | docs.total_rows mustEqual 3
68 | docs.rows must haveLength(3)
69 | docs.rows.map(_.id) mustEqual Seq(createdCarl.id, createdBob.id, createdAlice.id)
70 | docs.rows.map(_.key) mustEqual Seq(fixCarl.name, fixBob.name, fixAlice.name)
71 | docs.rows.map(_.value) mustEqual Seq(fixCarl.name, fixBob.name, fixAlice.name)
72 | }
73 |
74 | "Query a temporary view with compound keys and values" >> {
75 | val docs = awaitRight(compoundView.build.query)
76 | docs.offset mustEqual 0
77 | docs.total_rows mustEqual 3
78 | docs.rows must haveLength(3)
79 | docs.rows.map(_.key) mustEqual Seq((20, "Carl"), (25, "Alice"), (30, "Bob"))
80 | docs.rows.map(_.value) must contain(allOf(fixAlice, fixBob, fixCarl))
81 | }
82 |
83 | "Query a temporary view and select by key" >> {
84 | val docs1 = awaitRight(namesView.key("Alice").build.query)
85 | docs1.offset mustEqual 0
86 | docs1.total_rows mustEqual 3
87 | docs1.rows must haveLength(1)
88 | docs1.rows.head.key mustEqual "Alice"
89 | docs1.rows.head.value mustEqual "Alice"
90 | val docs2 = awaitRight(compoundView.key((30, "Bob")).build.query)
91 | docs2.offset mustEqual 2
92 | docs2.total_rows mustEqual 3
93 | docs2.rows must haveLength(1)
94 | docs2.rows.head.key mustEqual ((30, "Bob"))
95 | }
96 |
97 | "Query a temporary view and include documents" >> {
98 | val docs = awaitRight(namesView.includeDocs[FixPerson].build.query)
99 | docs.offset mustEqual 0
100 | docs.total_rows mustEqual 3
101 | docs.rows must haveLength(3)
102 | docs.rows.map(_.key) mustEqual Seq(fixAlice.name, fixBob.name, fixCarl.name)
103 | docs.rows.map(_.value) mustEqual Seq(fixAlice.name, fixBob.name, fixCarl.name)
104 | docs.rows.map(_.doc.doc) mustEqual Seq(fixAlice, fixBob, fixCarl)
105 | }
106 |
107 | "Query a temporary view with a set of keys" >> {
108 | val docs = awaitRight(namesView.withIds(Seq(fixAlice.name, fixBob.name)).build.query)
109 | docs.offset mustEqual 0
110 | docs.total_rows mustEqual 3
111 | docs.rows must haveLength(2)
112 | docs.rows.map(_.key) mustEqual Seq(fixAlice.name, fixBob.name)
113 | docs.rows.map(_.value) mustEqual Seq(fixAlice.name, fixBob.name)
114 | }
115 |
116 | "Query a temporary view with a set of keys and include documents" >> {
117 | val docs = awaitRight(
118 | namesView.includeDocs[FixPerson].withIds(
119 | Seq(fixAlice.name, fixBob.name)).build.query)
120 | docs.offset mustEqual 0
121 | docs.total_rows mustEqual 3
122 | docs.rows must haveLength(2)
123 | docs.rows.map(_.key) mustEqual Seq(fixAlice.name, fixBob.name)
124 | docs.rows.map(_.value) mustEqual Seq(fixAlice.name, fixBob.name)
125 | docs.rows.map(_.doc.doc) mustEqual Seq(fixAlice, fixBob)
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/test/scala/com/ibm/couchdb/api/QueryViewSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.api
18 |
19 | import com.ibm.couchdb.spec.CouchDbSpecification
20 | import com.ibm.couchdb.{CouchDocs, CouchKeyVals, CouchReducedKeyVals}
21 | import org.specs2.matcher.MatchResult
22 |
23 | import scalaz.concurrent.Task
24 |
25 | class QueryViewSpec extends CouchDbSpecification {
26 |
27 | val db = "couchdb-scala-query-view-spec"
28 | val databases = new Databases(client)
29 | val design = new Design(client, db)
30 | val documents = new Documents(client, db, typeMapping)
31 | val query = new Query(client, db)
32 | val namesView = query.view[String, String](fixDesign.name, FixViews.names).get
33 | val compoundView = query.view[(Int, String), FixPerson](fixDesign.name, FixViews.compound).get
34 | val aggregateView = query.view[String, String](fixDesign.name, FixViews.reduced).get
35 |
36 | recreateDb(databases, db)
37 |
38 | val createdDesign = awaitRight(design.create(fixDesign))
39 | val createdAlice = awaitRight(documents.create(fixAlice))
40 | val createdBob = awaitRight(documents.create(fixBob))
41 | val createdCarl = awaitRight(documents.create(fixCarl))
42 |
43 | "Query View API" >> {
44 |
45 | "Query a view" >> {
46 | def verify(task: Task[CouchKeyVals[String, String]]): MatchResult[Any] = {
47 | val docs = awaitRight(task)
48 | docs.offset mustEqual 0
49 | docs.total_rows mustEqual 3
50 | docs.rows must haveLength(3)
51 | docs.rows.map(_.id) mustEqual Seq(createdAlice.id, createdBob.id, createdCarl.id)
52 | docs.rows.map(_.key) mustEqual Seq(fixAlice.name, fixBob.name, fixCarl.name)
53 | docs.rows.map(_.value) mustEqual Seq(fixAlice.name, fixBob.name, fixCarl.name)
54 | }
55 | verify(namesView.build.query)
56 | verify(namesView.noReduce.excludeDocs.build.query)
57 | }
58 |
59 | "Query a view with reducer" >> {
60 | def verify(task: Task[CouchReducedKeyVals[String, Int]]): MatchResult[Any] = {
61 | val docs = awaitRight(task)
62 | docs.rows must haveLength(1)
63 | docs.rows.head.value mustEqual Seq(fixCarl.age, fixBob.age, fixAlice.age).sum
64 | }
65 | verify(aggregateView.reduce[Int].build.query)
66 | verify(aggregateView.noReduce.reduce[Int].build.query)
67 | }
68 |
69 | "Query a view with reducer given keys" >> {
70 | def verify(task: Task[CouchReducedKeyVals[String, Int]]): MatchResult[Any] = {
71 | val docs = awaitRight(task)
72 | docs.rows must haveLength(2)
73 | docs.rows.map(_.value).sum mustEqual Seq(fixCarl.age, fixAlice.age).sum
74 | docs.rows.map(_.key) mustEqual Seq(createdCarl.id, createdAlice.id)
75 | }
76 | verify(aggregateView.reduce[Int].withIds(Seq(createdCarl.id, createdAlice.id)).build.query)
77 | }
78 |
79 | "Query a view in the descending order" >> {
80 | def verify(task: Task[CouchKeyVals[String, String]]): MatchResult[Any] = {
81 | val docs = awaitRight(task)
82 | docs.offset mustEqual 0
83 | docs.total_rows mustEqual 3
84 | docs.rows must haveLength(3)
85 | docs.rows.map(_.id) mustEqual Seq(createdCarl.id, createdBob.id, createdAlice.id)
86 | docs.rows.map(_.key) mustEqual Seq(fixCarl.name, fixBob.name, fixAlice.name)
87 | docs.rows.map(_.value) mustEqual Seq(fixCarl.name, fixBob.name, fixAlice.name)
88 | }
89 | verify(namesView.descending().build.query)
90 | }
91 |
92 | "Query a view with compound keys and values" >> {
93 | def verify(
94 | task: Task[CouchKeyVals[(Int, String), FixPerson]]): MatchResult[Seq[FixPerson]] = {
95 | val docs = awaitRight(task)
96 | docs.offset mustEqual 0
97 | docs.total_rows mustEqual 3
98 | docs.rows must haveLength(3)
99 | docs.rows.map(_.key) mustEqual Seq((20, "Carl"), (25, "Alice"), (30, "Bob"))
100 | docs.rows.map(_.value) must contain(allOf(fixAlice, fixBob, fixCarl))
101 | }
102 | verify(compoundView.build.query)
103 | }
104 |
105 | "Query a view and select by key" >> {
106 | def verify[K, V, I](
107 | task: Task[CouchKeyVals[K, V]], expected: Seq[I], total: Int): MatchResult[Any] = {
108 | val docs = awaitRight(task)
109 | docs.total_rows mustEqual total
110 | docs.rows must haveLength(expected.length)
111 | docs.rows.map(_.key) must containTheSameElementsAs(expected)
112 | }
113 | verify(namesView.key("Alice").build.query, Seq("Alice"), 3)
114 | verify(compoundView.key((30, "Bob")).build.query, Seq((30, "Bob")), 3)
115 | }
116 |
117 | "Query a view and include documents" >> {
118 | def verify(task: Task[CouchDocs[String, String, FixPerson]]): MatchResult[Any] = {
119 | val docs = awaitRight(task)
120 | docs.offset mustEqual 0
121 | docs.total_rows mustEqual 3
122 | docs.rows must haveLength(3)
123 | docs.rows.map(_.key) mustEqual Seq(fixAlice.name, fixBob.name, fixCarl.name)
124 | docs.rows.map(_.value) mustEqual Seq(fixAlice.name, fixBob.name, fixCarl.name)
125 | docs.rows.map(_.doc.doc) mustEqual Seq(fixAlice, fixBob, fixCarl)
126 | }
127 | verify(namesView.includeDocs[FixPerson].build.query)
128 | }
129 |
130 | "Query a view with a set of keys" >> {
131 | def verify(task: Task[CouchKeyVals[String, String]]): MatchResult[Any] = {
132 | val docs = awaitRight(task)
133 | docs.offset mustEqual 0
134 | docs.total_rows mustEqual 3
135 | docs.rows must haveLength(2)
136 | docs.rows.map(_.key) mustEqual Seq(fixAlice.name, fixBob.name)
137 | docs.rows.map(_.value) mustEqual Seq(fixAlice.name, fixBob.name)
138 | }
139 | verify(namesView.withIds(Seq(fixAlice.name, fixBob.name)).build.query)
140 | }
141 |
142 | "Query a view with a set of keys and include documents" >> {
143 | def verify(task: Task[CouchDocs[String, String, FixPerson]]): MatchResult[Any] = {
144 | val docs = awaitRight(task)
145 | docs.offset mustEqual 0
146 | docs.total_rows mustEqual 3
147 | docs.rows must haveLength(2)
148 | docs.rows.map(_.key) mustEqual Seq(fixAlice.name, fixBob.name)
149 | docs.rows.map(_.value) mustEqual Seq(fixAlice.name, fixBob.name)
150 | docs.rows.map(_.doc.doc) mustEqual Seq(fixAlice, fixBob)
151 | }
152 | verify(namesView.includeDocs[FixPerson].withIds(Seq(fixAlice.name, fixBob.name)).build.query)
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/test/scala/com/ibm/couchdb/api/ServerSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.api
18 |
19 | import com.ibm.couchdb.spec.CouchDbSpecification
20 |
21 | class ServerSpec extends CouchDbSpecification {
22 |
23 | val db = "couchdb-scala-server-spec"
24 | val server = new Server(client)
25 |
26 | "Server API" >> {
27 |
28 | "Get info about the DB instance" >> {
29 | val info = awaitRight(server.info)
30 | info.couchdb mustEqual "Welcome"
31 | info.uuid must beUuid
32 | info.version.length must beGreaterThanOrEqualTo(3)
33 | }
34 |
35 | "Create a UUID" >> {
36 | awaitRight(server.mkUuid) must beUuid
37 | }
38 |
39 | "Create 3 UUIDs" >> {
40 | val uuids = awaitRight(server.mkUuids(3))
41 | uuids must have size 3
42 | uuids must contain(beUuid).forall
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/test/scala/com/ibm/couchdb/implicits/TaskImplicitsSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.implicits
18 |
19 | import com.ibm.couchdb.api.Databases
20 | import com.ibm.couchdb.spec.CouchDbSpecification
21 |
22 | class TaskImplicitsSpec extends CouchDbSpecification {
23 |
24 | val db = "couchdb-scala-task-implicits-spec"
25 | val databases = new Databases(client)
26 |
27 | private def clear() = await(databases.delete(db))
28 |
29 | "Task implicits" >> {
30 |
31 | "Ignore error" >> {
32 | clear()
33 | awaitOk(databases.create(db))
34 | awaitError(databases.create(db), "file_exists")
35 | awaitOk(databases.create(db).ignoreError)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/test/scala/com/ibm/couchdb/implicits/UpickleImplicitsSpec.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.implicits
18 |
19 | import com.ibm.couchdb.spec.Fixtures
20 | import org.http4s.Status
21 | import org.specs2.mutable._
22 | import org.specs2.specification.AllExpectations
23 | import upickle.default.Aliases.{R, W}
24 | import upickle.default.{read => pickleR, write => pickleW}
25 |
26 | class UpickleImplicitsSpec extends Specification
27 | with AllExpectations
28 | with Fixtures
29 | with UpickleImplicits {
30 |
31 | val map0 = Map[String, String]()
32 | val json0 = "{}"
33 |
34 | val map1 = Map[String, String]("key1" -> "val1", "key2" -> "val2")
35 | val json1 = "{\"key1\":\"val1\",\"key2\":\"val2\"}"
36 |
37 | val map2 = Map[String, Int]("key1" -> 1, "key2" -> 2)
38 | val json2 = "{\"key1\":1,\"key2\":2}"
39 |
40 | val map3 = Map[String, (String, Int)]("key1" -> (("key1", 1)), "key2" -> (("key2", 2)))
41 | val json3 = "{\"key1\":[\"key1\",1],\"key2\":[\"key2\",2]}"
42 |
43 | val map4 = Map[String, FixPerson](
44 | "key1" -> FixPerson("Alice", 25), "key2" -> FixPerson("Bob", 30))
45 | val json4 = "{\"key1\":{\"name\":\"Alice\",\"age\":25},\"key2\":{\"name\":\"Bob\",\"age\":30}}"
46 |
47 | private def testRoundtrip[D](obj: D)(implicit r: R[D], w: W[D]) = {
48 | pickleR[D](pickleW(obj)) mustEqual obj
49 | }
50 |
51 | "Custom upickle Reader and Writer instances" >> {
52 |
53 | "Write and read Map[String, D]" >> {
54 | pickleW(map0) mustEqual json0
55 | pickleW(map1) mustEqual json1
56 | pickleW(map2) mustEqual json2
57 | pickleW(map3) mustEqual json3
58 | pickleW(map4) mustEqual json4
59 | }
60 |
61 | "Read Map[String, D] from JSON" >> {
62 | pickleR[Map[String, String]](json0) mustEqual map0
63 | pickleR[Map[String, String]](json1) mustEqual map1
64 | pickleR[Map[String, Int]](json2) mustEqual map2
65 | pickleR[Map[String, (String, Int)]](json3) mustEqual map3
66 | pickleR[Map[String, FixPerson]](json4) mustEqual map4
67 | }
68 |
69 | "Write and read an Status" >> {
70 | testRoundtrip[Status](Status.Ok)
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/test/scala/com/ibm/couchdb/spec/CouchDbSpecification.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.spec
18 |
19 | import com.ibm.couchdb._
20 | import com.ibm.couchdb.api.Databases
21 | import com.ibm.couchdb.core.Client
22 | import com.ibm.couchdb.implicits.{TaskImplicits, UpickleImplicits}
23 | import org.specs2.matcher._
24 | import org.specs2.mutable._
25 | import org.specs2.scalaz.DisjunctionMatchers
26 | import org.specs2.specification.AllExpectations
27 |
28 | import scalaz._
29 | import scalaz.concurrent.Task
30 |
31 | trait CouchDbSpecification extends Specification with
32 | Fixtures with
33 | AllExpectations with
34 | DisjunctionMatchers with
35 | MatcherMacros with
36 | TaskImplicits with
37 | UpickleImplicits {
38 | sequential
39 |
40 | val client = new Client(
41 | Config(SpecConfig.couchDbHost, SpecConfig.couchDbPort, https = false, None))
42 |
43 | def await[T](future: Task[T]): Throwable \/ T = future.unsafePerformSyncAttempt
44 |
45 | def awaitRight[T](future: Task[T]): T = {
46 | val res = await(future)
47 | res must beRightDisjunction
48 | res.toOption.get
49 | }
50 |
51 | def awaitOk[T](future: Task[Res.Ok]): MatchResult[Any] = {
52 | await(future) must beRightDisjunction(Res.Ok(ok = true))
53 | }
54 |
55 | def awaitDocOk[D](future: Task[Res.DocOk]): MatchResult[Any] = {
56 | checkDocOk(awaitRight(future))
57 | }
58 |
59 | def awaitDocOk[D](future: Task[Res.DocOk], id: String): MatchResult[Any] = {
60 | checkDocOk(awaitRight(future), id)
61 | }
62 |
63 | def awaitLeft(future: Task[_]): Res.Error = {
64 | val res = await(future)
65 | res must beLeftDisjunction
66 | val -\/(e) = res
67 | e.asInstanceOf[CouchException[Res.Error]].content
68 | }
69 |
70 | def awaitError(future: Task[_], error: String): MatchResult[Any] = {
71 | val res = awaitLeft(future)
72 | res must beLike {
73 | case Res.Error(actualError, _, _, _, _) => actualError mustEqual error
74 | }
75 | }
76 |
77 | def beUuid: Matcher[String] = haveLength(32)
78 |
79 | def beRev: Matcher[String] = (_: String).length must beGreaterThan(32)
80 |
81 | def checkDocOk(doc: Res.DocOk): MatchResult[Any] = {
82 | (doc.ok mustEqual true) and (doc.id must not beEmpty) and (doc.rev must beRev)
83 | }
84 |
85 | def checkDocOk(doc: Res.DocOk, id: String): MatchResult[Any] = {
86 | checkDocOk(doc) and (doc.id mustEqual id)
87 | }
88 |
89 | def recreateDb(databases: Databases, name: String): \/[Throwable, Unit] = await {
90 | for {
91 | _ <- databases.delete(name).or(Task.now(Res.Ok()))
92 | _ <- databases.create(name)
93 | } yield ()
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/test/scala/com/ibm/couchdb/spec/Fixtures.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation, Google Inc.
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 |
17 | package com.ibm.couchdb.spec
18 |
19 | import com.ibm.couchdb.Lenses._
20 | import com.ibm.couchdb._
21 | import monocle.macros.GenLens
22 | import org.http4s.MediaType
23 | import org.http4s.headers.`Content-Type`
24 |
25 | trait Fixtures {
26 |
27 | case class FixPerson(name: String, age: Int)
28 |
29 | case class FixXPerson(name: String, aka: String, superPower: String)
30 |
31 | val typeMapping = TypeMapping(classOf[FixPerson] -> "Person", classOf[FixXPerson] -> "XPerson")
32 |
33 | val lenser = GenLens[FixPerson]
34 | val _personName = lenser(_.name)
35 | val _personAge = lenser(_.age)
36 | val _docPersonName = _couchDoc composeLens _personName
37 | val _docPersonAge = _couchDoc composeLens _personAge
38 |
39 | val fixProfessorX = FixXPerson(
40 | "Charles Xavier", "Professor X", "Telepathy, Astral projection, Mind control")
41 | val fixMagneto = FixXPerson("Max Eisenhardt", "Magneto", "Magnetism manipulation")
42 |
43 | val fixAlice = FixPerson("Alice", 25)
44 | val fixBob = FixPerson("Bob", 30)
45 | val fixCarl = FixPerson("Carl", 20)
46 | val fixHaile = FixPerson("\u1283\u12ED\u120C \u1308\u1265\u1228\u1225\u120B\u1234", 42)
47 | val fixMagritte = FixPerson("Ren\u00E9 Magritte", 68)
48 |
49 | object FixViews {
50 | val names = "names"
51 | val compound = "compound"
52 | val reduced = "reduced"
53 | val typeFilter = "typeFilter"
54 | val typeFilterCustom = "typeFilterCustom"
55 |
56 | val namesView = CouchView(
57 | map =
58 | """
59 | |function(doc) {
60 | | emit(doc.doc.name, doc.doc.name);
61 | |}
62 | """.stripMargin)
63 |
64 | val reducedView = CouchView(
65 | map =
66 | """
67 | |function(doc) {
68 | | emit(doc._id, doc.doc.age);
69 | |}
70 | """.stripMargin,
71 | reduce =
72 | """
73 | |function(key, values, rereduce) {
74 | | return sum(values);
75 | |}
76 | """.stripMargin)
77 |
78 | val compoundView = CouchView(
79 | map =
80 | """
81 | |function(doc) {
82 | | var d = doc.doc;
83 | | emit([d.age, d.name], d);
84 | |}
85 | """.stripMargin)
86 |
87 | val typeFilterView = CouchView(
88 | map =
89 | """
90 | |function(doc) {
91 | | emit([doc.kind, doc._id], doc._id);
92 | |}
93 | """.stripMargin)
94 |
95 | val typeFilterViewCustom = CouchView(
96 | map =
97 | """
98 | |function(doc) {
99 | | emit([doc.kind, doc._id, doc.doc.age], doc._id);
100 | |}
101 | """.stripMargin)
102 | }
103 |
104 | object FixShows {
105 | val csv = "csv"
106 | }
107 |
108 | object FixLists {
109 | val csvAll = "csv-all"
110 | }
111 |
112 | val fixDesign = CouchDesign(
113 | name = "test-design",
114 |
115 | views = Map(
116 | FixViews.names -> FixViews.namesView,
117 | FixViews.reduced -> FixViews.reducedView,
118 | FixViews.compound -> FixViews.compoundView,
119 | FixViews.typeFilter -> FixViews.typeFilterView,
120 | FixViews.typeFilterCustom -> FixViews.typeFilterViewCustom
121 | ),
122 |
123 | shows = Map(
124 | FixShows.csv ->
125 | """
126 | |function(doc, req) {
127 | | if (doc !== null && doc.kind == "Person") {
128 | | var res = doc.doc.name + ',' + doc.doc.age;
129 | | if (typeof req.query.extra !== "undefined") {
130 | | res += ',' + req.query.extra;
131 | | }
132 | | return res;
133 | | } else {
134 | | return 'empty show';
135 | | }
136 | |}
137 | """.stripMargin),
138 |
139 | lists = Map(
140 | FixLists.csvAll ->
141 | """
142 | |function(head, req) {
143 | | var row = getRow();
144 | | if (!row) {
145 | | return 'no rows';
146 | | }
147 | | if (typeof req.query.header !== "undefined" && req.query.header) {
148 | | send('name,age\n');
149 | | }
150 | | send(row.value.name + ',' + row.value.age + '\n');
151 | | while (row = getRow()) {
152 | | send(row.value.name + ',' + row.value.age + '\n');
153 | | }
154 | |}
155 | """.stripMargin)
156 |
157 | )
158 |
159 | val fixAttachmentName = "attachment"
160 | val fixAttachmentData = Array[Byte](-1, 0, 1, 2, 3)
161 | val fixAttachmentContentType = "image/jpg"
162 | val fixAttachment2Name = "attachment2"
163 | val fixAttachment2Data = Array[Byte](-1, 0, 1, 2, 3, 4, 5)
164 | val fixAttachment2ContentType = `Content-Type`(MediaType.`image/png`)
165 | }
166 |
--------------------------------------------------------------------------------
/src/test/scala/com/ibm/couchdb/spec/SpecConfig.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 IBM Corporation
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 |
17 | package com.ibm.couchdb.spec
18 |
19 | object SpecConfig {
20 |
21 | val couchDbHost = System.getProperty("couchDbHost", "127.0.0.1")
22 | val couchDbPort = System.getProperty("couchDbPort", "5984").toInt
23 | val couchDbHttpsPort = System.getProperty("couchDbHttpsPort", "6984").toInt
24 | val couchDbUsername = System.getProperty("couchDbUsername", "admin")
25 | val couchDbPassword = System.getProperty("couchDbPassword", "admin")
26 |
27 | private val log = org.log4s.getLogger
28 |
29 | log.info("----------------------")
30 | log.info(s"couchDbHost: $couchDbHost")
31 | log.info(s"couchDbPort: $couchDbPort")
32 | log.info("----------------------")
33 | }
34 |
--------------------------------------------------------------------------------