├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── doc
├── connecting.md
├── get-doc.md
├── index.md
├── put-doc.md
└── views.md
├── project.clj
├── resources
└── META-INF
│ └── mime.types
├── src
└── com
│ └── ashafa
│ ├── clutch.clj
│ └── clutch
│ ├── cljs_views.clj
│ ├── http_client.clj
│ ├── utils.clj
│ └── view_server.clj
└── test
├── clojure.png
├── clutch
└── test
│ ├── changes.clj
│ ├── type.clj
│ ├── views.clj
│ └── views
│ └── util.cljs
├── couchdb.png
└── test_clutch.clj
/.gitignore:
--------------------------------------------------------------------------------
1 | # various editor-related files
2 | *~
3 | .*.sw*
4 | .#*
5 | \#*
6 | nb-configuration.xml
7 | .externalToolBuilders
8 | .lein*
9 | .project
10 | .settings
11 | .classpath
12 |
13 | # build artifacts
14 | *.jar
15 | build
16 | target
17 | lib
18 | classes
19 | autodoc
20 | # cljs temporary output directory
21 | out
22 |
23 | # clojars
24 | pom.xml
25 | *clojars.org
26 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: clojure
2 | lein: lein2
3 | script: "lein2 all test"
4 | services:
5 | - couchdb
6 |
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2009-2012 Tunde Ashafa and other contributors
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions
6 | are met:
7 | 1. Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | 2. Redistributions in binary form must reproduce the above copyright
10 | notice, this list of conditions and the following disclaimer in the
11 | documentation and/or other materials provided with the distribution.
12 | 3. The name of the author may not be used to endorse or promote products
13 | derived from this software without specific prior written permission.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
16 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
17 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
18 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT
19 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
20 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE
21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
24 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Clutch [](http://travis-ci.org/#!/clojure-clutch/clutch/builds)
2 |
3 | Clutch is a [Clojure](http://clojure.org) library for [Apache CouchDB](http://couchdb.apache.org/).
4 |
5 | ## "Installation"
6 |
7 | To include Clutch in your project, simply add the following to your `project.clj` dependencies:
8 |
9 | ```clojure
10 | [com.ashafa/clutch "0.4.0"]
11 | ```
12 |
13 | Or, if you're using Maven, add this dependency to your `pom.xml`:
14 |
15 | ```
16 |
17 | com.ashafa
18 | clutch
19 | 0.4.0
20 |
21 | ```
22 |
23 | Clutch is compatible with Clojure 1.2.0+, and requires Java 1.5+.
24 |
25 | ## Status
26 |
27 | Although it's in an early stage of development (Clutch API subject to change), Clutch supports most of the Apache CouchDB API:
28 |
29 | * Essentially all of the [core document API](http://wiki.apache.org/couchdb/HTTP_Document_API)
30 | * [Bulk document APIs](http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API)
31 | * Most of the [Database API](http://wiki.apache.org/couchdb/HTTP_database_API), including `_changes` and a way to easily monitor/react to `_changes` events using Clojure's familiar watch mechanism
32 | * [Views](http://wiki.apache.org/couchdb/HTTP_view_API), including access, update, and a Clojure view server implementation
33 |
34 | Read the [documentation](doc/index.md) to learn the basics of Clutch. You can also look at the source or introspect the docs once you've loaded Clutch in your REPL.
35 |
36 | Clutch does not currently provide any direct support for the various couchapp-related APIs, including update handlers and validation, shows and lists, and so on.
37 |
38 | That said, it is very easy to call whatever CouchDB API feature that Clutch doesn't support using the lower-level `com.ashafa.clutch.http-client/couchdb-request` function.
39 |
40 | ## Usage
41 |
42 | First, a basic REPL interaction:
43 |
44 | ```clojure
45 | => (get-database "clutch_example") ;; creates database if it's not available yet
46 | #cemerick.url.URL{:protocol "http", :username nil, :password nil, :host "localhost", :port -1,
47 | :path "clutch_example", :query nil, :disk_format_version 5, :db_name "clutch_example", :doc_del_count 0,
48 | :committed_update_seq 0, :disk_size 79, :update_seq 0, :purge_seq 0, :compact_running false,
49 | :instance_start_time "1323701753566374", :doc_count 0}
50 |
51 | => (bulk-update "clutch_example" [{:test-grade 10 :_id "foo"}
52 | {:test-grade 20}
53 | {:test-grade 30}])
54 | [{:id "foo", :rev "1-8a15da0db077cd05b45ec93b3a207d09"}
55 | {:id "0896fbf57128d7f1a1b238a52b0ec372", :rev "1-796ebf042b42fa3585332c3aa4a6f706"}
56 | {:id "0896fbf57128d7f1a1b238a52b0ecda8", :rev "1-01f063c5aeb1b63992c90c72c7a515ed"}]
57 | => (get-document "clutch_example" "foo")
58 | {:_id "foo", :_rev "1-8a15da0db077cd05b45ec93b3a207d09", :test-grade 10}
59 | ```
60 |
61 | All Clutch functions accept a first argument indicating the database
62 | endpoint for that operation. This argument can be:
63 |
64 | * a `cemerick.url.URL` record instance (from the
65 | [url](http://github.com/cemerick/url) library)
66 | * a string URL
67 | * the name of the database to target on `http://localhost:5984`
68 |
69 | You can `assoc` in whatever you like to a `URL` record, which is handy for keeping database URLs and
70 | credentials separate:
71 |
72 | ```clojure
73 | => (def db (assoc (cemerick.url/url "https://XXX.cloudant.com/" "databasename")
74 | :username "username"
75 | :password "password"))
76 | #'test-clutch/db
77 | => (put-document db {:a 5 :b [0 6]})
78 | {:_id "17e55bcc31e33dd30c3313cc2e6e5bb4", :_rev "1-a3517724e42612f9fbd350091a96593c", :a 5, :b [0 6]}
79 | ```
80 |
81 | Of course, you can use a string containing inline credentials as well:
82 |
83 | ```clojure
84 | => (put-document "https://username:password@XXX.cloudant.com/databasename/" {:a 5 :b 6})
85 | {:_id "36b807aacf227f921aa256b06ab094e5", :_rev "1-d4d04a5b59bcd73893a84de2d9595c4c", :a 5, :b 6}
86 | ```
87 |
88 | Finally, you can optionally provide configuration using dynamic scope via `with-db`:
89 |
90 | ```clojure
91 | => (with-db "clutch_example"
92 | (put-document {:_id "a" :a 5})
93 | (put-document {:_id "b" :b 6})
94 | (-> (get-document "a")
95 | (merge (get-document "b"))
96 | (dissoc-meta)))
97 | {:b 6, :a 5}
98 | ```
99 |
100 | ### Experimental: a Clojure-idiomatic CouchDB type
101 |
102 | Clutch provides a pretty comprehensive API, but 95% of database
103 | interactions require using something other than the typical Clojure vocabulary of
104 | `assoc`, `conj`, `dissoc`, `get`, `seq`, `reduce`, etc, even though those semantics are entirely appropriate
105 | (modulo the whole stateful database thing).
106 |
107 | This is (the start of) an attempt to create a type to provide most of the
108 | functionality of Clutch with a more pleasant, concise API (it uses the Clutch API
109 | under the covers, and rare operations will generally remain accessible only
110 | at that lower level).
111 |
112 | Would like to eventually add:
113 |
114 | * support for views (aside from `_all_docs` via `seq`)
115 | * support for `_changes` (via a `seque`?), maybe a more natural place
116 | than the (free-for-all) pool of watches in Clutch's current API
117 | * support for bulk update, maybe via `IReduce`?
118 | * Other CouchDB types: ** to provide specialized query interfaces e.g.
119 | cloudant indexes ** to return custom map and vector types to support
120 | e.g.
121 |
122 | ```clojure
123 | (assoc-in! db ["ID" :key :key array-index] x)
124 | (update-in! db ["ID" :key :key array-index] assoc :key y)
125 | ```
126 |
127 | Feedback wanted on the mailing list: http://groups.google.com/group/clojure-clutch
128 |
129 | This part of the API is subject to change at any time, so no detailed examples. For now, just a REPL interaction will do:
130 |
131 | ```clojure
132 | => (use 'com.ashafa.clutch) ;; My apologies for the bare `use`!
133 | nil
134 | => (def db (couch "test"))
135 | #'user/db
136 | => (create! db)
137 | #
138 | => (:result (meta *1))
139 | #com.ashafa.clutch.utils.URL{:protocol "http", :username nil, :password nil,
140 | :host "localhost", :port -1, :path "test", :query nil, :disk_format_version 5,
141 | :db_name "test", :doc_del_count 0, :committed_update_seq 0, :disk_size 79,
142 | :update_seq 0, :purge_seq 0, :compact_running false, :instance_start_time
143 | "1324037686108297", :doc_count 0}
144 | => (reduce conj! db (for [x (range 5000)]
145 | {:_id (str x) :a [1 2 x]}))
146 | #
147 | => (count db)
148 | 5000
149 | => (get-in db ["68" :a 2])
150 | 68
151 | => (def copy (into {} db))
152 | #'user/copy
153 | => (get-in copy ["68" :a 2])
154 | 68
155 | => (first db)
156 | ["0" {:_id "0", :_rev "1-79fe783154bff972172bc30732783a68", :a [1 2 0]}]
157 | => (dissoc! db "68")
158 | #
159 | => (get db "68")
160 | nil
161 | => (assoc! db :foo {:a 6 :b 7})
162 | #
163 | => (:result (meta *1))
164 | {:_rev "1-ac3fe57a7604cfd6dcca06b25204b590", :_id ":foo", :a 6, :b 7}
165 | ```
166 |
167 | ### Using ClojureScript to write CouchDB views
168 |
169 | You can write your views/filters/validators in Clojure(Script) —
170 | avoiding the use of any special view server, special configuration, or
171 | JavaScript!
172 |
173 | Depending on the requirements of your view functions (e.g. if your views
174 | have no specific dependencies on Clojure or JVM libraries), then writing
175 | your views in ClojureScript can have a number of benefits:
176 |
177 | 1. No need to configure CouchDB instances to use the Clojure/Clutch view
178 | server.
179 | 2. Therefore, flexibility to use hosted CouchDB services like
180 | [Cloudant](http://cloudant.com), [Iris Couch](http://www.iriscouch.com/), et al.
181 | 3. Did we say 'no JavaScript'? Yup, no JavaScript. :-)
182 |
183 | #### "Installation"
184 |
185 | Clutch provides everything necessary to use ClojureScript to define
186 | CouchDB views, but it does not declare a specific ClojureScript
187 | dependency. This allows you to bring your own revision of ClojureScript
188 | into your project, and manage it without worrying about dependency
189 | management conflicts and such.
190 |
191 | You can always look at Clutch's `project.clj` to see which version of
192 | ClojureScript it is currently using to test its view support (
193 | `[org.clojure/clojurescript "0.0-1011"]` as of this writing).
194 |
195 | **Note that while Clutch itself only requires Clojure >= 1.2.0 ClojureScript
196 | requires Clojure >= 1.4.0.**
197 |
198 | The above requirement applies only if you are _saving_ ClojureScript
199 | views. A Clutch client using Clojure 1.2.0 can _access_ views written
200 | in ClojureScript (i.e. via `get-view`) without any dependence on
201 | ClojureScript at all.
202 |
203 | If you attempt to save a ClojureScript view but ClojureScript is not
204 | available (or you are using Clojure 1.2.x), an error will result.
205 |
206 | #### Usage
207 |
208 | Use Clutch's `save-view` per usual, but instead of providing a string of
209 | JavaScript (and specifying the language to be `:javascript`), provide a
210 | snippet of ClojureScript (specifying the language to be `:cljs`):
211 |
212 | ```clojure
213 | (with-db "your_database"
214 | (save-view "design_document_name"
215 | (view-server-fns :cljs
216 | {:your-view-name {:map (fn [doc]
217 | (js/emit (aget doc "_id") nil))}})))
218 | ```
219 |
220 | (Note that `view-server-fns` is a macro, so you do not need to quote
221 | your ClojureScript forms.)
222 |
223 | That's an example of a silly view, but should demonstrate the general
224 | pattern. Note the `js/emit` function; after ClojureScript compilation,
225 | this results in a call to the `emit` function defined by the standard
226 | CouchDB Javascript view server for emitting an entry into the view
227 | result. Follow the same conventions for reduce functions, filter
228 | functions, validator functions, etc.
229 |
230 | Your views can utilize larger codebases; just include your "top-level"
231 | ClojureScript forms in a vector:
232 |
233 | ```clojure
234 | (with-db "your_database"
235 | (save-view "design_document_name"
236 | (view-server-fns {:language :cljs
237 | :main 'couchview/main}
238 | {:your-view-name {:map [(ns couchview)
239 | (defn concat
240 | [id rev]
241 | (str id rev))
242 | (defn ^:export main
243 | [doc]
244 | (js/emit (concat (aget doc "_id") (aget doc "_rev")) nil))]}})))
245 | ```
246 |
247 | The `ns` form here can require other ClojureScript files on your
248 | classpath, refer to macros, etc. When using this longer form, remember
249 | to do three things:
250 |
251 | 1. You must provide a map of options to `view-server-fns`; `:cljs`
252 | becomes the `:language` value here.
253 | 2. Specify the "entry point" for the view function via the `:main` slot,
254 | `'couchview/main` here. This must correspond to an exported, defined
255 | function loaded by some ClojureScript, either in your vector literal of
256 | in-line ClojureScript, or in some ClojureScript loaded via a `:require`.
257 | 3. Ensure that your "entry point" function is exported; here, `main` is
258 | our entry point, exported via the `^:export` metadata.
259 |
260 | These last two points are required because of the default ClojureScript
261 | compilation option of `:advanced` optimizations.
262 |
263 | #### Compilation options
264 |
265 | The `view-server-fns` macro provided by Clutch takes as its first
266 | argument some options to pass along to the view transformer specified in
267 | that options map's `:language` slot. The `:cljs` transformer passes
268 | this options map along to the ClojureScript/Google Closure compiler,
269 | with defaults of:
270 |
271 | ```clojure
272 | {:optimizations :advanced
273 | :pretty-print false}
274 | ```
275 |
276 | So you can e.g. disable `:advanced` optimizations and turn on
277 | pretty-printing by passing this options map to `view-server-fns`:
278 |
279 | ```clojure
280 | {:optimizations :simple
281 | :pretty-print true
282 | :language :cljs}
283 | ```
284 |
285 | #### Internals
286 |
287 | If you really want to see what Javascript ClojureScript is generating
288 | for your view function(s), call `com.ashafa.clutch.cljs-views/view` with
289 | an options map as described above (`nil` to accept the defaults) and
290 | either an anonymous function body or vector of ClojureScript top-level
291 | forms.
292 |
293 | #### Caveats
294 |
295 | * ClojureScript / Google Closure produces a _very_ large code footprint,
296 | even for the simplest of view functions. This is apparently an item
297 | of active development in ClojureScript.
298 | * In any case, the code size of a view function string should have
299 | little to no impact on runtime performance of that view. The only
300 | penalty to be paid should be in view server initialization, which should
301 | be relatively infrequent. Further, the vast majority of view runtime is
302 | dominated by IO and actual document processing, not the loading of a
303 | handful of JavaScript functions.
304 | * The version of Spidermonkey that is used by CouchDB (and Cloudant at
305 | the moment) does not treat regular expression literals properly — they
306 | work fine as arguments, e.g. `string.match(/foo/)`, but e.g.
307 | `/foo/.exec("string")` fails. Using the `RegExp()` function with a
308 | string argument _does_ work. [This is reportedly fixed in CouchDB
309 | 1.2.0](https://issues.apache.org/jira/browse/COUCHDB-577), though I
310 | haven't verified that.
311 | * If you are familiar with writing CouchDB views in JavaScript, you must
312 | keep a close eye on your ClojureScript/JavaScript interop. e.g.
313 | `(js/emit [1 2] true)` will do _nothing_, because `[1 2]` is a
314 | ClojureScript vector, not a JavaScript array. Similarly, the values
315 | passed to view functions are JavaScript objects and arrays, not
316 | ClojureScript maps and vectors. A later release of Clutch will likely
317 | include a set of ClojureScript helper functions and macros that will
318 | make the necessary conversions automatic.
319 |
320 | ### Configuring your CouchDB installation to use the Clutch view server
321 |
322 | _This section is only germane if you are going to use Clutch's
323 | **Clojure** (i.e. JVM Clojure) view server. If the views you need to
324 | write can be expressed using ClojureScript — i.e. they have no JVM or
325 | Clojure library dependencies — using Clutch's ClojureScript support to
326 | write views is generally recommended._
327 |
328 | CouchDB needs to know how to exec Clutch's view server. Getting this command string together can be tricky, especially given potential classpath complexity. You can either (a) produce an uberjar of your project, in which case the exec string will be something like:
329 |
330 | ```
331 | java -cp clojure.main -m com.ashafa.clutch.view-server
332 | ```
333 |
334 | or, (b) you can use the `com.ashafa.clutch.utils/view-server-exec-string` function to dump a likely-to-work exec string. For example:
335 |
336 | ```clojure
337 | user=> (use '[com.ashafa.clutch.view-server :only (view-server-exec-string)])
338 | nil
339 | user=> (println (view-server-exec-string))
340 | java -cp "clutch/src:clutch/test:clutch/classes:clutch/resources:clutch/lib/clojure-1.3.0-beta1.jar:clutch/lib/clojure-contrib-1.2.0.jar:clutch/lib/data.json-0.1.1.jar:clutch/lib/tools.logging-0.1.2.jar" clojure.main -m com.ashafa.clutch.view-server
341 | ```
342 |
343 | This function assumes that `java` is on CouchDB's PATH, and it's entirely possible that the classpath might not be quite right (esp. on Windows — the above only tested on OS X and Linux so far). In any case, you can test whether the view server exec string is working properly by trying it yourself and attempting to get it to echo back a log message:
344 |
345 | ```
346 | [catapult:~/dev/clutch] chas% java -cp "clutch/src:clutch/test:clutch/classes:clutch/resources:clutch/lib/clojure-1.3.0-beta1.jar:clutch/lib/clojure-contrib-1.2.0.jar:clutch/lib/data.json-0.1.1.jar:clutch/lib/tools.logging-0.1.2.jar" clojure.main -m com.ashafa.clutch.view-server
347 | ["log" "echo, please"]
348 | ["log",["echo, please"]]
349 | ```
350 |
351 | Enter the first JSON array, and hit return; the view server should immediately reply with the second JSON array. Anything else, and your exec string is flawed, or something else is wrong.
352 |
353 | Once you have a working exec string, you can use Clojure for views and filters by adding a view server configuration to CouchDB. This can be as easy as passing the exec string to the `com.ashafa.clutch/configure-view-server` function:
354 |
355 | ```clojure
356 | (configure-view-server (view-server-exec-string))
357 | ```
358 |
359 | Alternatively, use Futon to add the `clojure` query server language to your CouchDB instance's config.
360 |
361 | In the end, both of these methods add the exec string you provide it to the `local.ini` file of your CouchDB installation, which you can modify directly if you like (this is likely what you'll need to do for non-local/production CouchDB instances):
362 |
363 | ```
364 | [query_servers]
365 | clojure = java -cp …rest of your exec string…
366 | ```
367 |
368 | #### View server configuration & view API usage
369 |
370 | ```clojure
371 | => (configure-view-server "clutch_example" (com.ashafa.clutch.view-server/view-server-exec-string))
372 | ""
373 | => (save-view "clutch_example" "demo_views" (view-server-fns :clojure
374 | {:sum {:map (fn [doc] [[nil (:test-grade doc)]])
375 | :reduce (fn [keys values _] (apply + values))}}))
376 | {:_rev "1-ddc80a2c95e06b62dd2923663dc855aa", :views {:sum {:map "(fn [doc] [[nil (:test-grade doc)]])", :reduce "(fn [keys values _] (apply + values))"}}, :language :clojure, :_id "_design/demo_views"}
377 | => (-> (get-view "clutch_example" "demo_views" :sum) first :value)
378 | 60
379 | => (get-view "clutch_example" "demo_views" :sum {:reduce false})
380 | ({:id "0896fbf57128d7f1a1b238a52b0ec372", :key nil, :value 20}
381 | {:id "0896fbf57128d7f1a1b238a52b0ecda8", :key nil, :value 30}
382 | {:id "foo", :key nil, :value 10})
383 | => (map :value (get-view "clutch_example" "demo_views" :sum {:reduce false}))
384 | (20 30 10)
385 | ```
386 |
387 | Note that all view access functions (i.e. `get-view`, `all-documents`, etc) return a lazy seq of their results (corresponding to the `:rows` slot in the data that couchdb returns in its view data). Other values (e.g. `total_rows`, `offset`, etc) are added to the returned lazy seq as metadata.
388 |
389 | ```clojure
390 | => (meta (all-documents "databasename"))
391 | {:total_rows 20000, :offset 0}
392 | ```
393 |
394 | ### `_changes` support
395 |
396 | Clutch provides comprehensive support for CouchDB's `_changes` feature.
397 | There is a `com.ashafa.clutch/changes` function that provides direct
398 | access to it, but most uses of `_changes` will benefit from using the
399 | `change-agent` feature. This configures a Clojure agent to receive
400 | updates from the `_changes` feed; its state will be updated to be the
401 | latest event (change notification), and so it is easy to hook up however
402 | many functions as necessary to the agent as watches (a.k.a. callbacks).
403 |
404 | Here's a REPL interaction demonstrating this functionality:
405 |
406 | ```clojure
407 | => (require '[com.ashafa.clutch :as couch])
408 | nil
409 | => (couch/create-database "demo")
410 | #cemerick.url.URL{:protocol "http", :username nil, :password nil,
411 | :host "localhost", :port 5984, :path "/demo",
412 | :query nil, :anchor nil}
413 | => (def a (couch/change-agent "demo"))
414 | #'user/a
415 |
416 | ;; `start-changes` hooks the agent up to the database's `_changes` feed
417 | => (couch/start-changes a)
418 | #
419 | => (couch/put-document "demo" {:name "Chas"})
420 | {:_id "259239233e2c2d06f3e311ce5f5271c1", :_rev "1-24ccfd9600c215e32ceefdd06b25f62d", :name "Chas"}
421 |
422 | ;; each change becomes a new state within the agent:
423 | => @a
424 | {:seq 1, :id "259239233e2c2d06f3e311ce5f5271c1", :changes [{:rev "1-24ccfd9600c215e32ceefdd06b25f62d"}]}
425 |
426 | ;; use Clojure's watch facility to have functions called on each change
427 | => (add-watch a :echo (fn [key agent previous-change change]
428 | (println "change received:" change)))
429 | #
430 | => (couch/put-document "demo" {:name "Roger"})
431 | {:_id "259239233e2c2d06f3e311ce5f527a9d", :_rev "1-0c3db91854f26486d1c3922f1a651d86", :name "Roger"}
432 | change received: {:seq 2, :id 259239233e2c2d06f3e311ce5f527a9d, :changes [{:rev 1-0c3db91854f26486d1c3922f1a651d86}]}
433 | => (couch/bulk-update "demo" [{:x 1} {:y 2} {:z 3 :_id "some-id"}])
434 | [{:id "259239233e2c2d06f3e311ce5f527cd4", :rev "1-0785e9eb543380151003dc452c3a001a"} {:id "259239233e2c2d06f3e311ce5f527fa6", :rev "1-ef91d626f27dc5d224fd534e7b47da82"} {:id "some-id", :rev "1-178dbe6c7346ffc3af8811327d1336ff"}]
435 | change received: {:seq 3, :id 259239233e2c2d06f3e311ce5f527cd4, :changes [{:rev 1-0785e9eb543380151003dc452c3a001a}]}
436 | change received: {:seq 4, :id 259239233e2c2d06f3e311ce5f527fa6, :changes [{:rev 1-ef91d626f27dc5d224fd534e7b47da82}]}
437 | change received: {:seq 5, :id some-id, :changes [{:rev 1-178dbe6c7346ffc3af8811327d1336ff}]}
438 | => (couch/delete-document "demo" (couch/get-document "demo" "some-id"))
439 | {:ok true, :id "some-id", :rev "2-7a128852666329025f1fba1114628251"}
440 | change received: {:seq 6, :id some-id,
441 | :changes [{:rev 2-7a128852666329025f1fba1114628251}], :deleted true}
442 |
443 | ;; if you want to stop the flow of changes through the agent, use
444 | ;; `stop-changes`
445 | => (couch/stop-changes a)
446 | #
447 | ```
448 |
449 | `changes` and `change-agent` pass along all of the parameters accepted
450 | by `_changes`, so you can get changes since a given point in time,
451 | filter changes based on a view server function, get the full content of
452 | changed documents included in the feed, etc. See the official [CouchDB
453 | API documentation for `_changes`](http://wiki.apache.org/couchdb/HTTP_database_API#Changes) for details.
454 |
455 | ## (Partial) Changelog
456 |
457 | ##### 0.4.0
458 |
459 | * **API change**: `watch-changes`, `stop-changes`, and `changes-error`
460 | have been removed. See the usage section on changes above.
461 | The `_changes` API support now consists of:
462 | * `changes` to obtain a lazy seq of updates from `_changes` directly
463 | * `change-agent`, `start-changes`, and `stop-changes` for creating and
464 | then controlling the activity of a Clojure agent whose state
465 | reflects the latest row from a continuous or longpoll view of
466 | `_changes`.
467 | * **API change**: `com.ashafa.clutch.http-client/*response-code*` has
468 | been replaced by `*response*`. Rather than just being optionally bound
469 | to the response code provided by CouchDB, this var is `set!`ed to its
470 | complete clj-http response.
471 | * Added `document-exists?` function; same as `(boolean (get-document db "key"))`,
472 | but uses a `HEAD` request instead of a `GET` (handy for checking for the
473 | existence of very large documents).
474 | * Write CouchDB views in ClojureScript! All of the functionality of
475 | [clutch-clojurescript](https://github.com/clojure-clutch/clutch-clojurescript)
476 | has been merged into Clutch proper.
477 | * [cheshire](https://github.com/dakrone/cheshire) is now being used for
478 | all JSON operations.
479 | * [clj-http](https://github.com/dakrone/clj-http) is now being used for
480 | all HTTP operations.
481 |
482 | ##### 0.3.1
483 |
484 | * Added the CouchDB "type", providing a higher-level and more
485 | Clojuresque abstraction for most CouchDB operations.
486 | * byte arrays may now be used with `put-attachment` et al.
487 | * Clutch may now be used with Java 1.5 (in addition to 1.6+)
488 |
489 | ##### 0.3.0
490 |
491 | Many breaking changes to refine/simplify the API, clean up the implementation, and add additional features:
492 |
493 | Core API:
494 |
495 | * Renamed `create-document` => `put-document`; `put-document` now supports both creation and update of a document depending upon whether `:_id` and `:_rev` slots are present in the document you are saving.
496 | * Renamed `update-attachment` => `put-attachment`; `filename` and `mime-type` arguments now kwargs, `InputStream` can now be provided as attachment data
497 | * `update-document` semantics have been simplified for the case where an "update function" and arguments are supplied to work well with core Clojure functions like `update-in` and `assoc` (fixes issue #8) — e.g. can be used like `swap!` et al.
498 | * Optional `:id` and `:attachment` arguments to `put-document` (was `create-document`) are now specified via keyword arguments
499 | * Removed "update map" argument from `bulk-update` fn (replace with e.g. `(bulk-update db (map #(merge % update-map) documents)`)
500 | * Renamed `get-all-documents-meta` => `all-documents`
501 | * `com.ashafa.clutch.http-client/*response-code*` is no longer assumed to be an atom. Rather, it is `set!`-ed directly when it is thread-bound. (Fixes issue #29)
502 |
503 | View-related API:
504 |
505 | * All views (`get-view`, `all-documents`, etc) now return lazy seqs corresponding to the `:rows` slot in the view data returned by couch. Other values (e.g. `total_rows`, `offset`, etc) are added to the returned lazy seq as metadata.
506 | * elimination of inconsistency between APIs between `save-view` and `save-filter`. The names of individual views and filters are now part of the map provided to these functions, instead of sometimes being provided separately.
507 | * `:language` has been eliminated as part of the dynamically-bound configuration map
508 | * `with-clj-view-server` has been replaced by the more generic `view-server-fns` macro, which takes a `:language` keyword or map of options that includes a `:language` slot (e.g. `:clojure`, `:javascript`, etc), and a map of view/filter/validator names => functions.
509 | * A `view-transformer` multimethod is now available, which opens up clutch to dynamically support additional view server languages.
510 | * Moved `view-server-exec-string` to `com.ashafa.clutch.view-server` namespace
511 |
512 | ## Contributors
513 |
514 | Appreciations go out to:
515 |
516 | * [Chas Emerick](http://cemerick.com)
517 | * [Tunde Ashafa](http://ashafa.com/)
518 | * [Pierre Larochelle](http://github.com/pierrel)
519 | * [Matt Wilson](http://github.com/mattdw)
520 | * [Patrick Sullivan](http://github.com/WizardofWestmarch)
521 | * [Toni Batchelli](http://tbatchelli.org)
522 | * [Hugo Duncan](http://github.com/hugoduncan)
523 | * [Ryan Senior](http://github.com/senior)
524 |
525 | ## License
526 |
527 | BSD. See the LICENSE file at the root of this repository.
528 |
529 |
530 |
--------------------------------------------------------------------------------
/doc/connecting.md:
--------------------------------------------------------------------------------
1 | # Connecting to CouchDB
2 |
3 | All Clutch functions accept a first argument indicating the database endpoint, for example:
4 |
5 | ```clojure
6 | (require '[com.ashafa.clutch :as couch])
7 |
8 | (couch/get-document "wiki" "home-page")
9 | (couch/get-document "http://localhost:5984/wiki" "blogs")
10 | ```
11 |
12 | However, putting a connection string in every call to Clutch is a lot of typing and it's not good practice to include these kinds of details in your code. So let's improve this by using an environment variable.
13 |
14 | First define an environment variable in your `profiles.clj` file:
15 |
16 | ```
17 | {:profiles/dev {:env {:database-url "wiki"}}}
18 | ```
19 |
20 | Now you can use this for the connection string:
21 |
22 | ```clojure
23 | (require '[com.ashafa.clutch :as couch])
24 | (require '[environ.core :refer [env]])
25 |
26 | (define db (env :database-url))
27 | (couch/get-document db "home-page")
28 | ```
29 |
30 | You can find out more about using environment variables here: https://github.com/weavejester/environ
31 |
32 | We can go one step further and use a simple macro to factor out the `db` parameter altogether:
33 |
34 | ```clojure
35 | (defmacro with-db
36 | [& body]
37 | `(couch/with-db (env :database-url)
38 | ~@body))
39 | ```
40 |
41 | Now we can write code like this:
42 |
43 | ```clojure
44 | (with-db (couch/get-document "blogs"))
45 | ```
46 |
47 | Next: [Getting documents](get-doc.md)
--------------------------------------------------------------------------------
/doc/get-doc.md:
--------------------------------------------------------------------------------
1 | # Getting documents
2 |
3 | Following on from [connecting to the database](connecting.md) let's look at how we can retrieve documents from CouchDB.
4 |
5 | ## All documents
6 |
7 | To get a sequence of all documents in the database use `all-documents`. With no parameters this returns just the document `id`, `key` and latest revision.
8 |
9 | ```clojure
10 | > (with-db (couch/all-documents))
11 | ({:id "_design/page_graph",
12 | :key "_design/page_graph",
13 | :value {:rev "9-78833b3c2b993ab70729fe09416b787d"}}
14 | {:id "_design/pages",
15 | :key "_design/pages",
16 | :value {:rev "17-04590ec8d06f0af6dc37a8f209e6edd2"}}
17 | {:id "about",
18 | :key "about",
19 | :value {:rev "10-4379c9bff126a5854779d177b48c8d2f"}}
20 | {:id "about-wikis",
21 | :key "about-wikis",
22 | :value {:rev "7-b7a842842bbf0059b421f522851afeaf"}}
23 | {:id "blogs",
24 | :key "blogs",
25 | :value {:rev "5-a3bdf7a1d156acae6ae69530394acff9"}}
26 | {:id "ideal-wiki",
27 | :key "ideal-wiki",
28 | :value {:rev "7-9b321eca6d2aab8e934b089684662342"}}
29 |
30 | ;; ...
31 | )
32 | ```
33 |
34 | [//]: # (Add info about include more, eg. include_docs)
35 |
36 | ## Getting a single document
37 |
38 | The simplest form is `get-document` with a document id:
39 |
40 | ```clojure
41 | (with-db (couch/get-document "ideal-wiki"))
42 | ```
43 |
44 | This returns a hash-map like the one below (clearly dependent on the data in your database):
45 |
46 | ```clojure
47 | {:_id "ideal-wiki", :_rev "7-9b321eca6d2aab8e934b089684662342", :content "What's the Ideal Wiki?\r\n================\r\n\r\n## Essentials\r\n\r\nAs an editor...\r\n\r\n* Markdown or similar syntax. WYSIWYG is too complex and error prone.\r\n* Really easy to make new pages, e.g. with `[[Links Like This]]` or maybe LikeThis.\r\n* Version history so that changes are safe.\r\n* Adding images is easy enough.\r\n* Wiki sections, e.g. Recipes, HomeEd.\r\n\r\nAs a reader of the wiki...\r\n\r\n* Nice default presentation.\r\n* Good search.\r\n* My own navigation bar of favourite pages.\r\n\r\n## Nice to have\r\n\r\n\r\nAs an editor...\r\n\r\n* Page rename doesn't break existing links.\r\n* Tagging, and pages styled by tag\r\n* Broken links, or links to pages that don't yet exist, are highlighted.\r\n* Auto-resize of images + image gallery.", :tags ["todo"]}
48 | ```
49 |
50 | You can retrieve parts of the document in the usual Clojure style:
51 |
52 | ```clojure
53 | > (:tags (with-db (couch/get-document "ideal-wiki")))
54 | ["todo"]
55 | ```
56 |
57 | ## Document Revisions
58 |
59 | Notice that the document contains information on the revision. CouchDB always returns the latest revision, unless you specify an earlier one.
60 |
61 | You can get the list of revisions by adding `:revs true` to `get-document`:
62 |
63 | ```clojure
64 | > (with-db (couch/get-document "ideal-wiki" :revs true))
65 |
66 | {:_id "ideal-wiki",
67 | :_rev "7-9b321eca6d2aab8e934b089684662342",
68 | :content
69 | "What's the Ideal Wiki? ...",
70 | :tags ["todo"],
71 | :_revisions
72 | {:start 7,
73 | :ids
74 | ["9b321eca6d2aab8e934b089684662342"
75 | "fcb4d3cc5a8e683cc35eb64d8d1f8ba2"
76 | "88624c792c8d1d0824d7423427034b23"
77 | "39c4436858fde5dda4839118f41ef722"
78 | "360401f1e5899a1368db7eee06213a1f"
79 | "ca013f24088b896b21ab792a5a0d9b90"
80 | "9e64f6ca68a253ef2bf24dee536356bf"]}}
81 | ```
82 |
83 | And to get just the revisions -- these are newest first:
84 |
85 | ```clojure
86 | > (:ids (:_revisions (with-db (couch/get-document "ideal-wiki" :revs true))))
87 | ["9b321eca6d2aab8e934b089684662342" "fcb4d3cc5a8e683cc35eb64d8d1f8ba2" "88624c792c8d1d0824d7423427034b23" "39c4436858fde5dda4839118f41ef722" "360401f1e5899a1368db7eee06213a1f" "ca013f24088b896b21ab792a5a0d9b90" "9e64f6ca68a253ef2bf24dee536356bf"]
88 | ```
89 |
90 | To retrieve a revision use the option `:rev` with a revision ID:
91 |
92 | ```clojure
93 | > (with-db (couch/get-document "ideal-wiki"
94 | :rev "fcb4d3cc5a8e683cc35eb64d8d1f8ba2"))
95 | ```
96 |
97 | Did you notice that this threw an error? Specifying revision IDs is not as simple as it looks, before we fix this let's take a little diversion into error reporting...
98 |
99 | ## Error reporting
100 |
101 | In Emacs with Cider (and other environments?), errors generate a 400 response from the CouchDB server. There's a lot of detail and it's difficult to see the cause, but look in the body and you'll see reason, in this case: `bad_request` and `Invalid rev format`:
102 |
103 | ```clojure
104 | 2. Unhandled clojure.lang.ExceptionInfo
105 |
106 | 1. Caused by clojure.lang.ExceptionInfo
107 | clj-http: status 400
108 | {:status 400, :headers {"server" "CouchDB/1.6.1 (Erlang OTP/19)", "date" "Wed, 02 May 2018 13:12:37 GMT", "content-type" "text/plain; charset=utf-8", "content-length" "54", "cache-control" "must-revalidate"}, :body "{\"error\":\"bad_request\",\"reason\":\"Invalid rev format\"}\n", :request {:path "/wiki/ideal-wiki", :user-info nil, :follow-redirects true, :body-type nil, :protocol "http", :password nil, :conn-timeout 5000, :as :json, :username nil, :http-req #object[clj_http.core.proxy$org.apache.http.client.methods.HttpEntityEnclosingRequestBase$ff19274a 0x313052b1 "GET http://localhost:5984/wiki/ideal-wiki?rev=fcb4d3cc5a8e683cc35eb64d8d1f8ba2 HTTP/1.1"], ...}}
109 | ```
110 |
111 | You can get the `body` within the `data` of the last exception like this, which is a bit easier than searching through the exception output:
112 |
113 | ```clojure
114 | > (:body (.data *e))
115 | "{\"error\":\"bad_request\",\"reason\":\"Invalid rev format\"}\n"
116 | ```
117 |
118 | ## Back to document revisions
119 |
120 | So to retrieve a revision, you must specify the version index and the revision number. In the previous example the index starts at 7, and descends through the revision list as (7 6 5 4 3 2 1). Here's the fixed code:
121 |
122 | ```clojure
123 | > (with-db (couch/get-document "ideal-wiki"
124 | :rev "6-fcb4d3cc5a8e683cc35eb64d8d1f8ba2"))
125 | ```
126 |
127 | Next: [Putting documents](put-doc.md)
--------------------------------------------------------------------------------
/doc/index.md:
--------------------------------------------------------------------------------
1 | # Clutch Docs
2 |
3 | Clutch is a [Clojure](http://clojure.org) library for [Apache CouchDB](http://couchdb.apache.org/).
4 |
5 | These documents guide you through using Clutch in a Clojure app created in Leiningen.
6 |
7 | ## Index:
8 |
9 | * [Connecting to the database](connecting.md)
10 | * [Getting documents](get-doc.md)
11 | * [Putting documents](put-doc.md)
12 | * Finding documents with [views](views.md)
13 |
14 |
15 |
--------------------------------------------------------------------------------
/doc/put-doc.md:
--------------------------------------------------------------------------------
1 | # Putting documents
2 |
3 | CouchDB is an immutable database, which means that you cannot update existing documents, rather you create new versions to record changes. Most CouchDB operations return the latest revision of documents.
4 |
5 | ## Putting a new document
6 |
7 | Use the `put-document` function to put a new document - notice how you specify the `_id` and then any extra fields you wish to add.
8 |
9 | ```clojure
10 | (with-db (couch/put-document {:_id "a-new-page"
11 | :content "lorem ipsum"
12 | :tags (list "tag1" "tag2")}))
13 | ```
14 |
15 | To see what immutable means, run that command again:
16 |
17 | ```clojure
18 | > ;;; repeat command above
19 | ExceptionInfo clj-http: status 409 slingshot.support/stack-trace (support.clj:201)
20 | > (:body (.data *e))
21 | "{\"error\":\"conflict\",\"reason\":\"Document update conflict.\"}\n"
22 | ```
23 |
24 | ## Updating a document
25 |
26 | To update a document you put a new revision, so first find the current revision with `get-document`:
27 |
28 | ```clojure
29 | > (with-db (couch/get-document "a-new-page"))
30 | {:_id "a-new-page",
31 | :_rev "1-0d1b7dedd3123e3d349748f2acce65d5",
32 | :content "lorem ipsum",
33 | :tags ["tag1" "tag2"]}
34 | ```
35 |
36 | Now we can use this info to create a new version -- make sure you use the `_rev` value from your own output (not from the output above):
37 |
38 | ```clojure
39 | (with-db (couch/put-document {:_id "a-new-page"
40 | :_rev "1-0d1b7dedd3123e3d349748f2acce65d5"
41 | :content "Some new content"
42 | :tags (list "tag1" "tag2" "tag3")}))
43 | ```
44 |
45 | The `put-document` command outputs the document written:
46 |
47 | ```clojure
48 | {:_id "a-new-page", :_rev "2-baef2d1090628e045129c9f0513ce782", :content "Some new content", :tags ("tag1" "tag2" "tag3")}
49 | ```
50 |
51 | Next: Finding documents with [views](views.md)
--------------------------------------------------------------------------------
/doc/views.md:
--------------------------------------------------------------------------------
1 | # Finding documents with views
2 |
3 | Use views to index or summarise your data so that you can find what you need. The CouchDB database has a good [guide to views](http://guide.couchdb.org/draft/views.html), read that first then follow the rest of this document to see how to use them from Clojure.
4 |
5 | ## Create your views
6 |
7 | Use the admin page on your database, often here: http://127.0.0.1:5984/_utils/ to create a new view.
8 |
9 | Select your database, then click the 'View' dropdown, then 'Temporary View...', this opens up a split pane JavaScript editor, with space for a 'map' function on the left and a 'reduce' function on the right.
10 |
11 | Here's an example that produces a map from tags to document ids:
12 |
13 | ```javascript
14 | function(doc) {
15 | if ('tags' in doc) {
16 | doc.tags.forEach( function(tag) {
17 | emit(tag, doc._id );
18 | });
19 | }
20 | }
21 | ```
22 |
23 | Make sure you name and save your temporary view. As a guide, the design document can be named by the entity (in this case 'pages') and the view by the key (in this case 'by_tag').
24 |
25 | ## Calling views from your code
26 |
27 | Once you've created, tested and saved your new view, call it using `get-view`:
28 |
29 | ```clojure
30 | > (with-db (couch/get-view "pages" "by_tag" {:key "tag1"}))
31 | ({:id "a-new-page", :key "tag1", :value "a-new-page"}
32 | {:id "a-new-page-2", :key "tag1", :value "a-new-page-2"})
33 | ```
34 |
35 | As you can see, `get-view` returns a list of hash-maps that identify the matching documents.
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject com.ashafa/clutch "0.4.0"
2 | :description "A Clojure library for Apache CouchDB."
3 | :url "https://github.com/clojure-clutch/clutch/"
4 | :license {:name "BSD"
5 | :url "http://www.opensource.org/licenses/BSD-3-Clause"}
6 | :dependencies [[org.clojure/clojure "1.4.0"]
7 |
8 | [clj-http "0.5.5"]
9 | [cheshire "4.0.0"]
10 | [commons-codec "1.6"]
11 | [com.cemerick/url "0.0.6"]
12 |
13 | [org.clojure/clojurescript "0.0-1450" :optional true
14 | :exclusions [com.google.code.findbugs/jsr305
15 | com.googlecode.jarjar/jarjar
16 | junit
17 | org.apache.ant/ant
18 | org.json/json
19 | org.mozilla/rhino]]]
20 | :profiles {:dev {}
21 | :1.2.0 {:dependencies [[org.clojure/clojure "1.2.0"]]}
22 | :1.3.0 {:dependencies [[org.clojure/clojure "1.3.0"]]}
23 | :1.5.0 {:dependencies [[org.clojure/clojure "1.5.0-alpha6"]]}}
24 | :aliases {"all" ["with-profile" "dev,1.2.0:dev,1.3.0:dev:dev,1.5.0"]}
25 | :min-lein-version "2.0.0"
26 | :test-selectors {:default #(not= 'test-docid-encoding (:name %))
27 | :all (constantly true)})
28 |
--------------------------------------------------------------------------------
/resources/META-INF/mime.types:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | #
3 | # MIME-TYPES and the extensions that represent them
4 | #
5 | # This file is part of the "mime-support" package. Please send email (not a
6 | # bug report) to mime-support@packages.debian.org if you would like new types
7 | # and/or extensions to be added.
8 | #
9 | # The reason that all types are managed by the mime-support package instead
10 | # allowing individual packages to install types in much the same way as they
11 | # add entries in to the mailcap file is so these types can be referenced by
12 | # other programs (such as a web server) even if the specific support package
13 | # for that type is not installed.
14 | #
15 | # Users can add their own types if they wish by creating a ".mime.types"
16 | # file in their home directory. Definitions included there will take
17 | # precedence over those listed here.
18 | #
19 | # Note: Compression schemes like "gzip", "bzip", and "compress" are not
20 | # actually "mime-types". They are "encodings" and hence must _not_ have
21 | # entries in this file to map their extensions. The "mime-type" of an
22 | # encoded file refers to the type of data that has been encoded, not the
23 | # type of encoding.
24 | #
25 | ###############################################################################
26 |
27 |
28 | application/activemessage
29 | application/andrew-inset ez
30 | application/annodex anx
31 | application/applefile
32 | application/atom+xml atom
33 | application/atomcat+xml atomcat
34 | application/atomserv+xml atomsrv
35 | application/atomicmail
36 | application/batch-SMTP
37 | application/beep+xml
38 | application/bbolin lin
39 | application/cals-1840
40 | application/cap cap pcap
41 | application/commonground
42 | application/cu-seeme cu
43 | application/cybercash
44 | application/davmount+xml davmount
45 | application/dca-rft
46 | application/dec-dx
47 | application/docbook+xml
48 | application/dsptype tsp
49 | application/dvcs
50 | application/ecmascript es
51 | application/edi-consent
52 | application/edi-x12
53 | application/edifact
54 | application/eshop
55 | application/font-tdpfr
56 | application/futuresplash spl
57 | application/ghostview
58 | application/hta hta
59 | application/http
60 | application/hyperstudio
61 | application/iges
62 | application/index
63 | application/index.cmd
64 | application/index.obj
65 | application/index.response
66 | application/index.vnd
67 | application/iotp
68 | application/ipp
69 | application/isup
70 | application/java-archive jar
71 | application/java-serialized-object ser
72 | application/java-vm class
73 | application/javascript js
74 | application/m3g m3g
75 | application/mac-binhex40 hqx
76 | application/mac-compactpro cpt
77 | application/macwriteii
78 | application/marc
79 | application/mathematica nb nbp
80 | application/ms-tnef
81 | application/msaccess mdb
82 | application/msword doc dot
83 | application/news-message-id
84 | application/news-transmission
85 | application/ocsp-request
86 | application/ocsp-response
87 | application/octet-stream bin
88 | application/oda oda
89 | application/ogg ogx
90 | application/parityfec
91 | application/pdf pdf
92 | application/pgp-encrypted
93 | application/pgp-keys key
94 | application/pgp-signature pgp
95 | application/pics-rules prf
96 | application/pkcs10
97 | application/pkcs7-mime
98 | application/pkcs7-signature
99 | application/pkix-cert
100 | application/pkix-crl
101 | application/pkixcmp
102 | application/postscript ps ai eps espi epsf eps2 eps3
103 | application/prs.alvestrand.titrax-sheet
104 | application/prs.cww
105 | application/prs.nprend
106 | application/qsig
107 | application/rar rar
108 | application/rdf+xml rdf
109 | application/remote-printing
110 | application/riscos
111 | application/rss+xml rss
112 | application/rtf rtf
113 | application/sdp
114 | application/set-payment
115 | application/set-payment-initiation
116 | application/set-registration
117 | application/set-registration-initiation
118 | application/sgml
119 | application/sgml-open-catalog
120 | application/sieve
121 | application/slate
122 | application/smil smi smil
123 | application/timestamp-query
124 | application/timestamp-reply
125 | application/vemmi
126 | application/whoispp-query
127 | application/whoispp-response
128 | application/wita
129 | application/x400-bp
130 | application/xhtml+xml xhtml xht
131 | application/xml xml xsl xsd
132 | application/xml-dtd
133 | application/xml-external-parsed-entity
134 | application/xspf+xml xspf
135 | application/zip zip
136 | application/vnd.3M.Post-it-Notes
137 | application/vnd.accpac.simply.aso
138 | application/vnd.accpac.simply.imp
139 | application/vnd.acucobol
140 | application/vnd.aether.imp
141 | application/vnd.anser-web-certificate-issue-initiation
142 | application/vnd.anser-web-funds-transfer-initiation
143 | application/vnd.audiograph
144 | application/vnd.bmi
145 | application/vnd.businessobjects
146 | application/vnd.canon-cpdl
147 | application/vnd.canon-lips
148 | application/vnd.cinderella cdy
149 | application/vnd.claymore
150 | application/vnd.commerce-battelle
151 | application/vnd.commonspace
152 | application/vnd.comsocaller
153 | application/vnd.contact.cmsg
154 | application/vnd.cosmocaller
155 | application/vnd.ctc-posml
156 | application/vnd.cups-postscript
157 | application/vnd.cups-raster
158 | application/vnd.cups-raw
159 | application/vnd.cybank
160 | application/vnd.dna
161 | application/vnd.dpgraph
162 | application/vnd.dxr
163 | application/vnd.ecdis-update
164 | application/vnd.ecowin.chart
165 | application/vnd.ecowin.filerequest
166 | application/vnd.ecowin.fileupdate
167 | application/vnd.ecowin.series
168 | application/vnd.ecowin.seriesrequest
169 | application/vnd.ecowin.seriesupdate
170 | application/vnd.enliven
171 | application/vnd.epson.esf
172 | application/vnd.epson.msf
173 | application/vnd.epson.quickanime
174 | application/vnd.epson.salt
175 | application/vnd.epson.ssf
176 | application/vnd.ericsson.quickcall
177 | application/vnd.eudora.data
178 | application/vnd.fdf
179 | application/vnd.ffsns
180 | application/vnd.flographit
181 | application/vnd.framemaker
182 | application/vnd.fsc.weblaunch
183 | application/vnd.fujitsu.oasys
184 | application/vnd.fujitsu.oasys2
185 | application/vnd.fujitsu.oasys3
186 | application/vnd.fujitsu.oasysgp
187 | application/vnd.fujitsu.oasysprs
188 | application/vnd.fujixerox.ddd
189 | application/vnd.fujixerox.docuworks
190 | application/vnd.fujixerox.docuworks.binder
191 | application/vnd.fut-misnet
192 | application/vnd.google-earth.kml+xml kml
193 | application/vnd.google-earth.kmz kmz
194 | application/vnd.grafeq
195 | application/vnd.groove-account
196 | application/vnd.groove-identity-message
197 | application/vnd.groove-injector
198 | application/vnd.groove-tool-message
199 | application/vnd.groove-tool-template
200 | application/vnd.groove-vcard
201 | application/vnd.hhe.lesson-player
202 | application/vnd.hp-HPGL
203 | application/vnd.hp-PCL
204 | application/vnd.hp-PCLXL
205 | application/vnd.hp-hpid
206 | application/vnd.hp-hps
207 | application/vnd.httphone
208 | application/vnd.hzn-3d-crossword
209 | application/vnd.ibm.MiniPay
210 | application/vnd.ibm.afplinedata
211 | application/vnd.ibm.modcap
212 | application/vnd.informix-visionary
213 | application/vnd.intercon.formnet
214 | application/vnd.intertrust.digibox
215 | application/vnd.intertrust.nncp
216 | application/vnd.intu.qbo
217 | application/vnd.intu.qfx
218 | application/vnd.irepository.package+xml
219 | application/vnd.is-xpr
220 | application/vnd.japannet-directory-service
221 | application/vnd.japannet-jpnstore-wakeup
222 | application/vnd.japannet-payment-wakeup
223 | application/vnd.japannet-registration
224 | application/vnd.japannet-registration-wakeup
225 | application/vnd.japannet-setstore-wakeup
226 | application/vnd.japannet-verification
227 | application/vnd.japannet-verification-wakeup
228 | application/vnd.koan
229 | application/vnd.lotus-1-2-3
230 | application/vnd.lotus-approach
231 | application/vnd.lotus-freelance
232 | application/vnd.lotus-notes
233 | application/vnd.lotus-organizer
234 | application/vnd.lotus-screencam
235 | application/vnd.lotus-wordpro
236 | application/vnd.mcd
237 | application/vnd.mediastation.cdkey
238 | application/vnd.meridian-slingshot
239 | application/vnd.mif
240 | application/vnd.minisoft-hp3000-save
241 | application/vnd.mitsubishi.misty-guard.trustweb
242 | application/vnd.mobius.daf
243 | application/vnd.mobius.dis
244 | application/vnd.mobius.msl
245 | application/vnd.mobius.plc
246 | application/vnd.mobius.txf
247 | application/vnd.motorola.flexsuite
248 | application/vnd.motorola.flexsuite.adsi
249 | application/vnd.motorola.flexsuite.fis
250 | application/vnd.motorola.flexsuite.gotap
251 | application/vnd.motorola.flexsuite.kmr
252 | application/vnd.motorola.flexsuite.ttc
253 | application/vnd.motorola.flexsuite.wem
254 | application/vnd.mozilla.xul+xml xul
255 | application/vnd.ms-artgalry
256 | application/vnd.ms-asf
257 | application/vnd.ms-excel xls xlb xlt
258 | application/vnd.ms-lrm
259 | application/vnd.ms-pki.seccat cat
260 | application/vnd.ms-pki.stl stl
261 | application/vnd.ms-powerpoint ppt pps
262 | application/vnd.ms-project
263 | application/vnd.ms-tnef
264 | application/vnd.ms-works
265 | application/vnd.mseq
266 | application/vnd.msign
267 | application/vnd.music-niff
268 | application/vnd.musician
269 | application/vnd.netfpx
270 | application/vnd.noblenet-directory
271 | application/vnd.noblenet-sealer
272 | application/vnd.noblenet-web
273 | application/vnd.novadigm.EDM
274 | application/vnd.novadigm.EDX
275 | application/vnd.novadigm.EXT
276 | application/vnd.oasis.opendocument.chart odc
277 | application/vnd.oasis.opendocument.database odb
278 | application/vnd.oasis.opendocument.formula odf
279 | application/vnd.oasis.opendocument.graphics odg
280 | application/vnd.oasis.opendocument.graphics-template otg
281 | application/vnd.oasis.opendocument.image odi
282 | application/vnd.oasis.opendocument.presentation odp
283 | application/vnd.oasis.opendocument.presentation-template otp
284 | application/vnd.oasis.opendocument.spreadsheet ods
285 | application/vnd.oasis.opendocument.spreadsheet-template ots
286 | application/vnd.oasis.opendocument.text odt
287 | application/vnd.oasis.opendocument.text-master odm
288 | application/vnd.oasis.opendocument.text-template ott
289 | application/vnd.oasis.opendocument.text-web oth
290 | application/vnd.osa.netdeploy
291 | application/vnd.palm
292 | application/vnd.pg.format
293 | application/vnd.pg.osasli
294 | application/vnd.powerbuilder6
295 | application/vnd.powerbuilder6-s
296 | application/vnd.powerbuilder7
297 | application/vnd.powerbuilder7-s
298 | application/vnd.powerbuilder75
299 | application/vnd.powerbuilder75-s
300 | application/vnd.previewsystems.box
301 | application/vnd.publishare-delta-tree
302 | application/vnd.pvi.ptid1
303 | application/vnd.pwg-xhtml-print+xml
304 | application/vnd.rapid
305 | application/vnd.rim.cod cod
306 | application/vnd.s3sms
307 | application/vnd.seemail
308 | application/vnd.shana.informed.formdata
309 | application/vnd.shana.informed.formtemplate
310 | application/vnd.shana.informed.interchange
311 | application/vnd.shana.informed.package
312 | application/vnd.smaf mmf
313 | application/vnd.sss-cod
314 | application/vnd.sss-dtf
315 | application/vnd.sss-ntf
316 | application/vnd.stardivision.calc sdc
317 | application/vnd.stardivision.chart sds
318 | application/vnd.stardivision.draw sda
319 | application/vnd.stardivision.impress sdd
320 | application/vnd.stardivision.math sdf
321 | application/vnd.stardivision.writer sdw
322 | application/vnd.stardivision.writer-global sgl
323 | application/vnd.street-stream
324 | application/vnd.sun.xml.calc sxc
325 | application/vnd.sun.xml.calc.template stc
326 | application/vnd.sun.xml.draw sxd
327 | application/vnd.sun.xml.draw.template std
328 | application/vnd.sun.xml.impress sxi
329 | application/vnd.sun.xml.impress.template sti
330 | application/vnd.sun.xml.math sxm
331 | application/vnd.sun.xml.writer sxw
332 | application/vnd.sun.xml.writer.global sxg
333 | application/vnd.sun.xml.writer.template stw
334 | application/vnd.svd
335 | application/vnd.swiftview-ics
336 | application/vnd.symbian.install sis
337 | application/vnd.triscape.mxs
338 | application/vnd.trueapp
339 | application/vnd.truedoc
340 | application/vnd.tve-trigger
341 | application/vnd.ufdl
342 | application/vnd.uplanet.alert
343 | application/vnd.uplanet.alert-wbxml
344 | application/vnd.uplanet.bearer-choice
345 | application/vnd.uplanet.bearer-choice-wbxml
346 | application/vnd.uplanet.cacheop
347 | application/vnd.uplanet.cacheop-wbxml
348 | application/vnd.uplanet.channel
349 | application/vnd.uplanet.channel-wbxml
350 | application/vnd.uplanet.list
351 | application/vnd.uplanet.list-wbxml
352 | application/vnd.uplanet.listcmd
353 | application/vnd.uplanet.listcmd-wbxml
354 | application/vnd.uplanet.signal
355 | application/vnd.vcx
356 | application/vnd.vectorworks
357 | application/vnd.vidsoft.vidconference
358 | application/vnd.visio vsd
359 | application/vnd.vividence.scriptfile
360 | application/vnd.wap.sic
361 | application/vnd.wap.slc
362 | application/vnd.wap.wbxml wbxml
363 | application/vnd.wap.wmlc wmlc
364 | application/vnd.wap.wmlscriptc wmlsc
365 | application/vnd.webturbo
366 | application/vnd.wordperfect wpd
367 | application/vnd.wordperfect5.1 wp5
368 | application/vnd.wrq-hp3000-labelled
369 | application/vnd.wt.stf
370 | application/vnd.xara
371 | application/vnd.xfdl
372 | application/vnd.yellowriver-custom-menu
373 | application/x-123 wk
374 | application/x-7z-compressed 7z
375 | application/x-abiword abw
376 | application/x-apple-diskimage dmg
377 | application/x-bcpio bcpio
378 | application/x-bittorrent torrent
379 | application/x-cab cab
380 | application/x-cbr cbr
381 | application/x-cbz cbz
382 | application/x-cdf cdf cda
383 | application/x-cdlink vcd
384 | application/x-chess-pgn pgn
385 | application/x-core
386 | application/x-cpio cpio
387 | application/x-csh csh
388 | application/x-debian-package deb udeb
389 | application/x-director dcr dir dxr
390 | application/x-dms dms
391 | application/x-doom wad
392 | application/x-dvi dvi
393 | application/x-httpd-eruby rhtml
394 | application/x-executable
395 | application/x-font pfa pfb gsf pcf pcf.Z
396 | application/x-freemind mm
397 | application/x-futuresplash spl
398 | application/x-gnumeric gnumeric
399 | application/x-go-sgf sgf
400 | application/x-graphing-calculator gcf
401 | application/x-gtar gtar tgz taz
402 | application/x-hdf hdf
403 | application/x-httpd-php phtml pht php
404 | application/x-httpd-php-source phps
405 | application/x-httpd-php3 php3
406 | application/x-httpd-php3-preprocessed php3p
407 | application/x-httpd-php4 php4
408 | application/x-ica ica
409 | application/x-info info
410 | application/x-internet-signup ins isp
411 | application/x-iphone iii
412 | application/x-iso9660-image iso
413 | application/x-jam jam
414 | application/x-java-applet
415 | application/x-java-bean
416 | application/x-java-jnlp-file jnlp
417 | application/x-jmol jmz
418 | application/x-kchart chrt
419 | application/x-kdelnk
420 | application/x-killustrator kil
421 | application/x-koan skp skd skt skm
422 | application/x-kpresenter kpr kpt
423 | application/x-kspread ksp
424 | application/x-kword kwd kwt
425 | application/x-latex latex
426 | application/x-lha lha
427 | application/x-lyx lyx
428 | application/x-lzh lzh
429 | application/x-lzx lzx
430 | application/x-maker frm maker frame fm fb book fbdoc
431 | application/x-mif mif
432 | application/x-ms-wmd wmd
433 | application/x-ms-wmz wmz
434 | application/x-msdos-program com exe bat dll
435 | application/x-msi msi
436 | application/x-netcdf nc
437 | application/x-ns-proxy-autoconfig pac dat
438 | application/x-nwc nwc
439 | application/x-object o
440 | application/x-oz-application oza
441 | application/x-pkcs7-certreqresp p7r
442 | application/x-pkcs7-crl crl
443 | application/x-python-code pyc pyo
444 | application/x-qgis qgs shp shx
445 | application/x-quicktimeplayer qtl
446 | application/x-redhat-package-manager rpm
447 | application/x-ruby rb
448 | application/x-rx
449 | application/x-sh sh
450 | application/x-shar shar
451 | application/x-shellscript
452 | application/x-shockwave-flash swf swfl
453 | application/x-stuffit sit sitx
454 | application/x-sv4cpio sv4cpio
455 | application/x-sv4crc sv4crc
456 | application/x-tar tar
457 | application/x-tcl tcl
458 | application/x-tex-gf gf
459 | application/x-tex-pk pk
460 | application/x-texinfo texinfo texi
461 | application/x-trash ~ % bak old sik
462 | application/x-troff t tr roff
463 | application/x-troff-man man
464 | application/x-troff-me me
465 | application/x-troff-ms ms
466 | application/x-ustar ustar
467 | application/x-videolan
468 | application/x-wais-source src
469 | application/x-wingz wz
470 | application/x-x509-ca-cert crt
471 | application/x-xcf xcf
472 | application/x-xfig fig
473 | application/x-xpinstall xpi
474 |
475 | audio/32kadpcm
476 | audio/3gpp
477 | audio/amr amr
478 | audio/amr-wb awb
479 | audio/amr amr
480 | audio/amr-wb awb
481 | audio/annodex axa
482 | audio/basic au snd
483 | audio/flac flac
484 | audio/g.722.1
485 | audio/l16
486 | audio/midi mid midi kar
487 | audio/mp4a-latm
488 | audio/mpa-robust
489 | audio/mpeg mpga mpega mp2 mp3 m4a
490 | audio/mpegurl m3u
491 | audio/ogg oga ogg spx
492 | audio/parityfec
493 | audio/prs.sid sid
494 | audio/telephone-event
495 | audio/tone
496 | audio/vnd.cisco.nse
497 | audio/vnd.cns.anp1
498 | audio/vnd.cns.inf1
499 | audio/vnd.digital-winds
500 | audio/vnd.everad.plj
501 | audio/vnd.lucent.voice
502 | audio/vnd.nortel.vbk
503 | audio/vnd.nuera.ecelp4800
504 | audio/vnd.nuera.ecelp7470
505 | audio/vnd.nuera.ecelp9600
506 | audio/vnd.octel.sbc
507 | audio/vnd.qcelp
508 | audio/vnd.rhetorex.32kadpcm
509 | audio/vnd.vmx.cvsd
510 | audio/x-aiff aif aiff aifc
511 | audio/x-gsm gsm
512 | audio/x-mpegurl m3u
513 | audio/x-ms-wma wma
514 | audio/x-ms-wax wax
515 | audio/x-pn-realaudio-plugin
516 | audio/x-pn-realaudio ra rm ram
517 | audio/x-realaudio ra
518 | audio/x-scpls pls
519 | audio/x-sd2 sd2
520 | audio/x-wav wav
521 |
522 | chemical/x-alchemy alc
523 | chemical/x-cache cac cache
524 | chemical/x-cache-csf csf
525 | chemical/x-cactvs-binary cbin cascii ctab
526 | chemical/x-cdx cdx
527 | chemical/x-cerius cer
528 | chemical/x-chem3d c3d
529 | chemical/x-chemdraw chm
530 | chemical/x-cif cif
531 | chemical/x-cmdf cmdf
532 | chemical/x-cml cml
533 | chemical/x-compass cpa
534 | chemical/x-crossfire bsd
535 | chemical/x-csml csml csm
536 | chemical/x-ctx ctx
537 | chemical/x-cxf cxf cef
538 | #chemical/x-daylight-smiles smi
539 | chemical/x-embl-dl-nucleotide emb embl
540 | chemical/x-galactic-spc spc
541 | chemical/x-gamess-input inp gam gamin
542 | chemical/x-gaussian-checkpoint fch fchk
543 | chemical/x-gaussian-cube cub
544 | chemical/x-gaussian-input gau gjc gjf
545 | chemical/x-gaussian-log gal
546 | chemical/x-gcg8-sequence gcg
547 | chemical/x-genbank gen
548 | chemical/x-hin hin
549 | chemical/x-isostar istr ist
550 | chemical/x-jcamp-dx jdx dx
551 | chemical/x-kinemage kin
552 | chemical/x-macmolecule mcm
553 | chemical/x-macromodel-input mmd mmod
554 | chemical/x-mdl-molfile mol
555 | chemical/x-mdl-rdfile rd
556 | chemical/x-mdl-rxnfile rxn
557 | chemical/x-mdl-sdfile sd sdf
558 | chemical/x-mdl-tgf tgf
559 | #chemical/x-mif mif
560 | chemical/x-mmcif mcif
561 | chemical/x-mol2 mol2
562 | chemical/x-molconn-Z b
563 | chemical/x-mopac-graph gpt
564 | chemical/x-mopac-input mop mopcrt mpc zmt
565 | chemical/x-mopac-out moo
566 | chemical/x-mopac-vib mvb
567 | chemical/x-ncbi-asn1 asn
568 | chemical/x-ncbi-asn1-ascii prt ent
569 | chemical/x-ncbi-asn1-binary val aso
570 | chemical/x-ncbi-asn1-spec asn
571 | chemical/x-pdb pdb ent
572 | chemical/x-rosdal ros
573 | chemical/x-swissprot sw
574 | chemical/x-vamas-iso14976 vms
575 | chemical/x-vmd vmd
576 | chemical/x-xtel xtel
577 | chemical/x-xyz xyz
578 |
579 | image/cgm
580 | image/g3fax
581 | image/gif gif
582 | image/ief ief
583 | image/jpeg jpeg jpg jpe
584 | image/naplps
585 | image/pcx pcx
586 | image/png png
587 | image/prs.btif
588 | image/prs.pti
589 | image/svg+xml svg svgz
590 | image/tiff tiff tif
591 | image/vnd.cns.inf2
592 | image/vnd.djvu djvu djv
593 | image/vnd.dwg
594 | image/vnd.dxf
595 | image/vnd.fastbidsheet
596 | image/vnd.fpx
597 | image/vnd.fst
598 | image/vnd.fujixerox.edmics-mmr
599 | image/vnd.fujixerox.edmics-rlc
600 | image/vnd.mix
601 | image/vnd.net-fpx
602 | image/vnd.svf
603 | image/vnd.wap.wbmp wbmp
604 | image/vnd.xiff
605 | image/x-cmu-raster ras
606 | image/x-coreldraw cdr
607 | image/x-coreldrawpattern pat
608 | image/x-coreldrawtemplate cdt
609 | image/x-corelphotopaint cpt
610 | image/x-icon ico
611 | image/x-jg art
612 | image/x-jng jng
613 | image/x-ms-bmp bmp
614 | image/x-photoshop psd
615 | image/x-portable-anymap pnm
616 | image/x-portable-bitmap pbm
617 | image/x-portable-graymap pgm
618 | image/x-portable-pixmap ppm
619 | image/x-rgb rgb
620 | image/x-xbitmap xbm
621 | image/x-xpixmap xpm
622 | image/x-xwindowdump xwd
623 |
624 | inode/chardevice
625 | inode/blockdevice
626 | inode/directory-locked
627 | inode/directory
628 | inode/fifo
629 | inode/socket
630 |
631 | message/delivery-status
632 | message/disposition-notification
633 | message/external-body
634 | message/http
635 | message/s-http
636 | message/news
637 | message/partial
638 | message/rfc822 eml
639 |
640 | model/iges igs iges
641 | model/mesh msh mesh silo
642 | model/vnd.dwf
643 | model/vnd.flatland.3dml
644 | model/vnd.gdl
645 | model/vnd.gs-gdl
646 | model/vnd.gtw
647 | model/vnd.mts
648 | model/vnd.vtu
649 | model/vrml wrl vrml
650 |
651 | multipart/alternative
652 | multipart/appledouble
653 | multipart/byteranges
654 | multipart/digest
655 | multipart/encrypted
656 | multipart/form-data
657 | multipart/header-set
658 | multipart/mixed
659 | multipart/parallel
660 | multipart/related
661 | multipart/report
662 | multipart/signed
663 | multipart/voice-message
664 |
665 | text/calendar ics icz
666 | text/css css
667 | text/csv csv
668 | text/directory
669 | text/english
670 | text/enriched
671 | text/h323 323
672 | text/html html htm shtml
673 | text/iuls uls
674 | text/mathml mml
675 | text/parityfec
676 | text/plain asc txt text pot brf
677 | text/prs.lines.tag
678 | text/rfc822-headers
679 | text/richtext rtx
680 | text/rtf
681 | text/scriptlet sct wsc
682 | text/t140
683 | text/texmacs tm ts
684 | text/tab-separated-values tsv
685 | text/uri-list
686 | text/vnd.abc
687 | text/vnd.curl
688 | text/vnd.DMClientScript
689 | text/vnd.flatland.3dml
690 | text/vnd.fly
691 | text/vnd.fmi.flexstor
692 | text/vnd.in3d.3dml
693 | text/vnd.in3d.spot
694 | text/vnd.IPTC.NewsML
695 | text/vnd.IPTC.NITF
696 | text/vnd.latex-z
697 | text/vnd.motorola.reflex
698 | text/vnd.ms-mediapackage
699 | text/vnd.sun.j2me.app-descriptor jad
700 | text/vnd.wap.si
701 | text/vnd.wap.sl
702 | text/vnd.wap.wml wml
703 | text/vnd.wap.wmlscript wmls
704 | text/x-bibtex bib
705 | text/x-boo boo
706 | text/x-c++hdr h++ hpp hxx hh
707 | text/x-c++src c++ cpp cxx cc
708 | text/x-chdr h
709 | text/x-component htc
710 | text/x-crontab
711 | text/x-csh csh
712 | text/x-csrc c
713 | text/x-dsrc d
714 | text/x-diff diff patch
715 | text/x-haskell hs
716 | text/x-java java
717 | text/x-literate-haskell lhs
718 | text/x-makefile
719 | text/x-moc moc
720 | text/x-pascal p pas
721 | text/x-pcs-gcd gcd
722 | text/x-perl pl pm
723 | text/x-python py
724 | text/x-scala scala
725 | text/x-server-parsed-html
726 | text/x-setext etx
727 | text/x-sh sh
728 | text/x-tcl tcl tk
729 | text/x-tex tex ltx sty cls
730 | text/x-vcalendar vcs
731 | text/x-vcard vcf
732 |
733 | video/3gpp 3gp
734 | video/annodex axv
735 | video/dl dl
736 | video/dv dif dv
737 | video/fli fli
738 | video/gl gl
739 | video/mpeg mpeg mpg mpe
740 | video/mp4 mp4
741 | video/quicktime qt mov
742 | video/mp4v-es
743 | video/ogg ogv
744 | video/parityfec
745 | video/pointer
746 | video/vnd.fvt
747 | video/vnd.motorola.video
748 | video/vnd.motorola.videop
749 | video/vnd.mpegurl mxu
750 | video/vnd.mts
751 | video/vnd.nokia.interleaved-multimedia
752 | video/vnd.vivo
753 | video/x-flv flv
754 | video/x-la-asf lsf lsx
755 | video/x-mng mng
756 | video/x-ms-asf asf asx
757 | video/x-ms-wm wm
758 | video/x-ms-wmv wmv
759 | video/x-ms-wmx wmx
760 | video/x-ms-wvx wvx
761 | video/x-msvideo avi
762 | video/x-sgi-movie movie
763 | video/x-matroska mpv
764 |
765 | x-conference/x-cooltalk ice
766 |
767 | x-epoc/x-sisx-app sisx
768 | x-world/x-vrml vrm vrml wrl
769 |
--------------------------------------------------------------------------------
/src/com/ashafa/clutch.clj:
--------------------------------------------------------------------------------
1 | (ns com.ashafa.clutch
2 | (:require [com.ashafa.clutch [utils :as utils]]
3 | [cheshire.core :as json]
4 | [clojure.java.io :as io]
5 | [cemerick.url :as url]
6 | clojure.string)
7 | (:use com.ashafa.clutch.http-client)
8 | (:import (java.io File FileInputStream BufferedInputStream InputStream ByteArrayOutputStream)
9 | (java.net URL))
10 | (:refer-clojure :exclude (conj! assoc! dissoc!)))
11 |
12 | (def ^{:private true} highest-supported-charcode 0xfff0)
13 |
14 | (def ^{:doc "A very 'high' unicode character that can be used
15 | as a wildcard suffix when querying views."}
16 | ; \ufff0 appears to be the highest character that couchdb can support
17 | ; discovered experimentally with v0.10 and v0.11 ~March 2010
18 | ; now officially documented at http://wiki.apache.org/couchdb/View_collation
19 | wildcard-collation-string (str (char highest-supported-charcode)))
20 |
21 | (def ^{:dynamic true :private true} *database* nil)
22 |
23 | (declare couchdb-class)
24 |
25 | (defn- with-db*
26 | [f]
27 | (fn [& [maybe-db & rest :as args]]
28 | (let [maybe-db (if (instance? couchdb-class maybe-db)
29 | (.url maybe-db)
30 | maybe-db)]
31 | (if (and (thread-bound? #'*database*)
32 | (not (identical? maybe-db *database*)))
33 | (apply f *database* args)
34 | (apply f (utils/url maybe-db) rest)))))
35 |
36 | (defmacro ^{:private true} defdbop
37 | "Same as defn, but wraps the defined function in another that transparently
38 | allows for dynamic or explicit application of database configuration as well
39 | as implicit coercion of the first `db` argument to a URL instance."
40 | [name & body]
41 | `(do
42 | (defn ~name ~@body)
43 | (alter-var-root (var ~name) with-db*)
44 | (alter-meta! (var ~name) update-in [:doc] str
45 | "\n\n When used within the dynamic scope of `with-db`, the initial `db`"
46 | "\n argument is automatically provided.")))
47 |
48 | (defdbop couchdb-info
49 | "Returns information about a CouchDB instance."
50 | [db]
51 | (couchdb-request :get (utils/server-url db)))
52 |
53 | (defdbop all-databases
54 | "Returns a list of all databases on the CouchDB server."
55 | [db]
56 | (couchdb-request :get (url/url db "/_all_dbs")))
57 |
58 | (defdbop create-database
59 | [db]
60 | (couchdb-request :put db)
61 | db)
62 |
63 | (defdbop database-info
64 | [db]
65 | (couchdb-request :get db)
66 | #_(when-let [info (couchdb-request :get db)]
67 | (merge info
68 | (when-let [watchers (@watched-databases (str db))]
69 | {:watchers (keys watchers)}))))
70 |
71 | (defdbop get-database
72 | "Returns a database meta information if it already exists else creates a new database and returns
73 | the meta information for the new database."
74 | [db]
75 | (merge db
76 | (or (database-info db)
77 | (and (create-database db)
78 | (database-info db)))))
79 |
80 | (defdbop delete-database
81 | [db]
82 | (couchdb-request :delete db))
83 |
84 | (defdbop replicate-database
85 | "Takes two arguments (a source and target for replication) which could be a
86 | string (name of a database in the default Clutch configuration) or a map that
87 | contains a least the database name (':name' keyword, map is merged with
88 | default Clutch configuration) and reproduces all the active documents in the
89 | source database on the target databse."
90 | [srcdb tgtdb]
91 | (couchdb-request :post
92 | (url/url tgtdb "/_replicate")
93 | :data {:source (str srcdb)
94 | :target (str tgtdb)}))
95 |
96 | (def ^{:private true} byte-array-class (Class/forName "[B"))
97 |
98 | (defn- attachment-info
99 | ([{:keys [data filename mime-type data-length]}] (attachment-info data data-length filename mime-type))
100 | ([data data-length filename mime-type]
101 | (let [data (if (string? data)
102 | (File. ^String data)
103 | data)
104 | check (fn [k v]
105 | (if v v
106 | (throw (IllegalArgumentException.
107 | (str k " must be provided if attachment data is an InputStream or byte[]")))))]
108 | (cond
109 | (instance? File data)
110 | [(-> ^File data FileInputStream. BufferedInputStream.)
111 | (.length ^File data)
112 | (or filename (.getName ^File data))
113 | (or mime-type (utils/get-mime-type data))]
114 |
115 | (instance? InputStream data)
116 | [data (check :data-length data-length) (check :filename filename) (check :mime-type mime-type)]
117 |
118 | (= byte-array-class (class data))
119 | [(java.io.ByteArrayInputStream. data) (count data)
120 | (check :filename filename) (check :mime-type mime-type)]
121 |
122 | :default
123 | (throw (IllegalArgumentException. (str "Cannot handle attachment data of type " (class data))))))))
124 |
125 | (defn- to-byte-array
126 | [input]
127 | (if (= byte-array-class (class input))
128 | input
129 | ; make sure streams are closed so we don't hold locks on files on Windows
130 | (with-open [^InputStream input (io/input-stream input)]
131 | (let [out (ByteArrayOutputStream.)]
132 | (io/copy input out)
133 | (.toByteArray out)))))
134 |
135 | (defdbop put-document
136 | [db document & {:keys [id attachments request-params]}]
137 | (let [document (merge document
138 | (when id {:_id id})
139 | (when (seq attachments)
140 | (->> attachments
141 | (map #(if (map? %) % {:data %}))
142 | (map attachment-info)
143 | (reduce (fn [m [data data-length filename mime]]
144 | (assoc m (keyword filename)
145 | {:content_type mime
146 | :data (-> data
147 | to-byte-array
148 | org.apache.commons.codec.binary.Base64/encodeBase64String)}))
149 | {})
150 | (hash-map :_attachments))))
151 | result (couchdb-request
152 | (if (:_id document) :put :post)
153 | (assoc (utils/url db (:_id document))
154 | :query request-params)
155 | :data document)]
156 | (and (:ok result)
157 | (assoc document :_rev (:rev result) :_id (:id result)))))
158 |
159 | (defn dissoc-meta
160 | "dissoc'es the :_id and :_rev slots from the provided map."
161 | [doc]
162 | (dissoc doc :_id :_rev))
163 |
164 | (defdbop get-document
165 | "Returns the document identified by the given id. Optional CouchDB document API query parameters
166 | (rev, attachments, may be provided as keyword arguments."
167 | [db id & {:as get-params}]
168 | ;; TODO a nil or empty key should probably just throw an exception
169 | (when (seq (str id))
170 | (couchdb-request :get
171 | (-> (utils/url db id)
172 | (assoc :query get-params)))))
173 |
174 | (defdbop document-exists?
175 | "Returns true if a document with the given key exists in the database."
176 | [db id]
177 | ;; TODO a nil or empty key should probably just throw an exception
178 | (when (seq (str id))
179 | (= 200 (:status (couchdb-request* :head (utils/url db id))))))
180 |
181 | (defn- document?
182 | [x]
183 | (and (map? x) (:_id x) (:_rev x)))
184 |
185 | (defn- document-url
186 | [database-url document]
187 | (when-not (:_id document)
188 | (throw (IllegalArgumentException. "A valid document with an :_id slot is required.")))
189 | (let [with-id (utils/url database-url (:_id document))]
190 | (if-let [rev (:_rev document)]
191 | (assoc with-id :query (str "rev=" rev))
192 | with-id)))
193 |
194 | (defdbop delete-document
195 | "Takes a document and deletes it from the database."
196 | [db document]
197 | (couchdb-request :delete (document-url db document)))
198 |
199 | (defdbop copy-document
200 | "Copies the provided document (or the document with the given string id)
201 | to the given new id. If the destination id identifies an existing
202 | document, then a document map (with up-to-date :_id and :_rev slots)
203 | must be provided as a destination argument to avoid a 409 Conflict."
204 | [db src dest]
205 | (let [dest (if (map? dest) dest {:_id dest})
206 | ; TODO yuck; surely we need an id?rev=123 string elsewhere, so this can be rolled into a URL helper fn?
207 | dest (apply str (:_id dest) (when (:_rev dest) ["?rev=" (:_rev dest)]))]
208 | (couchdb-request :copy
209 | (document-url db (if (map? src)
210 | src
211 | {:_id src}))
212 | :headers {"Destination" dest})))
213 |
214 | ;; TODO update-document doesn't provide a lot, now that put-document is here and can update or create as necessary
215 | (defdbop update-document
216 | "Takes document and a map and merges it with the original. When a function
217 | and a vector of keys are supplied as the second and third argument, the
218 | value of the keys supplied are updated with the result of the function of
219 | their values (see: #'clojure.core/update-in)."
220 | [db document & [mod & args]]
221 | (let [document (cond
222 | (map? mod) (merge document mod)
223 | (fn? mod) (apply mod document args)
224 | (nil? mod) document
225 | :else (throw (IllegalArgumentException.
226 | "A map or function is needed to update a document.")))]
227 | (put-document db document)))
228 |
229 | (defdbop configure-view-server
230 | "Sets the query server exec string for views written in the specified :language
231 | (\"clojure\" by default). This is intended to be
232 | a REPL convenience function; see the Clutch README for more info about setting
233 | up CouchDB to be Clutch-view-server-ready in general terms."
234 | [db exec-string & {:keys [language] :or {language "clojure"}}]
235 | (couchdb-request :put
236 | (url/url db "/_config/query_servers/" (-> language name url/url-encode))
237 | :data (pr-str exec-string)))
238 |
239 | (defn- map-leaves
240 | [f m]
241 | (into {} (for [[k v] m]
242 | (if (map? v)
243 | [k (map-leaves f v)]
244 | [k (f v)]))))
245 |
246 | (defmulti view-transformer identity)
247 | (defmethod view-transformer :clojure
248 | [_]
249 | {:language :clojure
250 | :compiler (fn [options] pr-str)})
251 |
252 | (defmethod view-transformer :default
253 | [language]
254 | {:language language
255 | :compiler (fn [options] str)})
256 |
257 | (defmethod view-transformer :cljs
258 | [language]
259 | (try
260 | (require 'com.ashafa.clutch.cljs-views)
261 | ; com.ashafa.clutch.cljs-views defines a method for :cljs, so this
262 | ; call will land in it
263 | (view-transformer language)
264 | (catch Exception e
265 | (throw (UnsupportedOperationException.
266 | "Could not load com.ashafa.clutch.cljs-views; ensure ClojureScript and its dependencies are available, and that you're using Clojure >= 1.3.0." e)))))
267 |
268 | (defmacro view-server-fns
269 | [options fns]
270 | (let [[language options] (if (map? options)
271 | [(or (:language options) :javascript) (dissoc options :language)]
272 | [options])]
273 | [(:language (view-transformer language))
274 | `(#'map-leaves ((:compiler (view-transformer ~language)) ~options) '~fns)]))
275 |
276 | (defdbop save-design-document
277 | "Create/update a design document containing functions used for database
278 | queries/filtering/validation/etc."
279 | [db fn-type design-document-name [language view-server-fns]]
280 | (let [design-doc-id (str "_design/" design-document-name)
281 | ddoc {fn-type view-server-fns
282 | :language (name language)}]
283 | (if-let [design-doc (get-document db design-doc-id)]
284 | (update-document db design-doc merge ddoc)
285 | (put-document db (assoc ddoc :_id design-doc-id)))))
286 |
287 | (defdbop save-view
288 | "Create or update a design document containing views used for database queries."
289 | [db & args]
290 | (apply save-design-document db :views args))
291 |
292 | (defdbop save-filter
293 | "Create a filter for use with CouchDB change notifications API."
294 | [db & args]
295 | (apply save-design-document db :filters args))
296 |
297 | (defn- get-view*
298 | "Get documents associated with a design document. Also takes an optional map
299 | for querying options, and a second map of {:key [keys]} to be POSTed.
300 | (see: http://wiki.apache.org/couchdb/HTTP_view_API)."
301 | [db path-segments & [query-params-map post-data-map]]
302 | (let [url (assoc (apply utils/url db path-segments)
303 | :query (into {} (for [[k v] query-params-map]
304 | [k (if (#{"key" "keys" "startkey" "endkey"} (name k))
305 | (json/generate-string v)
306 | v)])))]
307 | (view-request
308 | (if (empty? post-data-map) :get :post)
309 | url
310 | :data (when (seq post-data-map) post-data-map))))
311 |
312 | (defdbop get-view
313 | "Get documents associated with a design document. Also takes an optional map
314 | for querying options, and a second map of {:key [keys]} to be POSTed.
315 | (see: http://wiki.apache.org/couchdb/HTTP_view_API)."
316 | [db design-document view-key & [query-params-map post-data-map :as args]]
317 | (apply get-view* db
318 | ["_design" design-document "_view" (name view-key)]
319 | args))
320 |
321 | (defdbop all-documents
322 | "Returns the meta (_id and _rev) of all documents in a database. By adding
323 | the key ':include_docs' with a value of true to the optional query params map
324 | you can also get the full documents, not just their meta. Also takes an optional
325 | second map of {:keys [keys]} to be POSTed.
326 | (see: http://wiki.apache.org/couchdb/HTTP_view_API)."
327 | [db & [query-params-map post-data-map :as args]]
328 | (apply get-view* db ["_all_docs"] args))
329 |
330 | (defdbop ad-hoc-view
331 | "One-off queries (i.e. views you don't want to save in the CouchDB database). Ad-hoc
332 | views should be used only during development. Also takes an optional map for querying
333 | options (see: http://wiki.apache.org/couchdb/HTTP_view_API)."
334 | [db [language view-server-fns] & [query-params-map]]
335 | (get-view* db ["_temp_view"] query-params-map (into {:language language} view-server-fns)))
336 |
337 | (defdbop bulk-update
338 | "Takes a sequential collection of documents (maps) and inserts or updates (if \"_id\" and \"_rev\" keys
339 | are supplied in a document) them with in a single request.
340 |
341 | Optional keyword args may be provided, and are sent along with the documents
342 | (e.g. for \"all-or-nothing\" semantics, etc)."
343 | [db documents & {:as options}]
344 | (couchdb-request :post
345 | (utils/url db "_bulk_docs")
346 | :data (assoc options :docs documents)))
347 |
348 | (defdbop put-attachment
349 | "Updates (or creates) the attachment for the given document. `data` can be a string path
350 | to a file, a java.io.File, a byte array, or an InputStream.
351 |
352 | If `data` is a byte array or InputStream, you _must_ include the following otherwise-optional
353 | kwargs:
354 |
355 | :filename — name to be given to the attachment in the document
356 | :mime-type — type of attachment data
357 |
358 | These are derived from a file path or File if not provided. (Mime types are derived from
359 | filename extensions; see com.ashafa.clutch.utils/get-mime-type for determining mime type
360 | yourself from a File object.)"
361 | [db document data & {:keys [filename mime-type data-length]}]
362 | (let [[stream data-length filename mime-type] (attachment-info data data-length filename mime-type)]
363 | (couchdb-request :put
364 | (-> db
365 | (document-url document)
366 | (utils/url (name filename)))
367 | :data stream
368 | :data-length data-length
369 | :content-type mime-type)))
370 |
371 | (defdbop delete-attachment
372 | "Deletes an attachemnt from a document."
373 | [db document file-name]
374 | (couchdb-request :delete (utils/url (document-url db document) file-name)))
375 |
376 | (defdbop get-attachment
377 | "Returns an InputStream reading the named attachment to the specified/provided document
378 | or nil if the document or attachment does not exist.
379 |
380 | Hint: use the copy or to-byte-array fns in c.c.io to easily redirect the result."
381 | [db doc-or-id attachment-name]
382 | (let [doc (if (map? doc-or-id) doc-or-id (get-document db doc-or-id))
383 | attachment-name (if (keyword? attachment-name)
384 | (name attachment-name)
385 | attachment-name)]
386 | (when (-> doc :_attachments (get (keyword attachment-name)))
387 | (couchdb-request :get
388 | (-> (document-url db doc)
389 | (utils/url attachment-name)
390 | (assoc :as :stream))))))
391 |
392 | ;;;; _changes
393 |
394 | (defdbop changes
395 | "Returns a lazy seq of the rows in _changes, as configured by the given options.
396 |
397 | If you want to react to change notifications, you should probably use `change-agent`."
398 | [db & {:keys [since limit descending feed heartbeat
399 | timeout filter include_docs style] :as opts}]
400 | (let [url (url/url db "_changes")
401 | response (couchdb-request* :get (assoc url :query opts :as :stream))]
402 | (when-not response
403 | (throw (IllegalStateException. (str "Database for _changes feed does not exist: " url))))
404 | (-> response
405 | :body
406 | (lazy-view-seq (not= "continuous" feed))
407 | (vary-meta assoc ::http-resp response))))
408 |
409 | (defn- change-agent-config
410 | [db options]
411 | (merge {:heartbeat 30000 :feed "continuous"}
412 | options
413 | (when-not (:since options)
414 | {:since (:update_seq (database-info db))})
415 | {::db db
416 | ::state :init
417 | ::last-update-seq nil}))
418 |
419 | (defdbop change-agent
420 | "Returns an agent whose state will very to contain events
421 | emitted by the _changes feed of the specified database.
422 |
423 | Users are expected to attach functions to the agent using
424 | `add-watch` in order to be notified of changes. The
425 | returned change agent is entirely 'managed', with
426 | `start-changes` and `stop-changes` controlling its operation
427 | and sent actions. If you send actions to a change agent,
428 | bad things will likely happen."
429 | [db & {:as opts}]
430 | (agent nil :meta {::changes-config (atom (change-agent-config db opts))}))
431 |
432 | (defn- run-changes
433 | [_]
434 | (let [config-atom (-> *agent* meta ::changes-config)
435 | config @config-atom]
436 | (case (::state config)
437 | :init (let [changes (apply changes (::db config) (flatten (remove (comp namespace key) config)))
438 | http-resp (-> changes meta ::http-resp)]
439 | ; cannot shut down continuous _changes feeds without aborting this
440 | (assert (-> http-resp :request :http-req))
441 | (swap! config-atom merge {::seq changes
442 | ::http-resp http-resp
443 | ::state :running})
444 | (send-off *agent* #'run-changes)
445 | nil)
446 | :running (let [change-seq (::seq config)
447 | change (first change-seq)
448 | last-change-seq (or (:seq change) (:last_seq change))]
449 | (send-off *agent* #'run-changes)
450 | (when-not (= :stopped (::state @config-atom))
451 | (swap! config-atom merge
452 | {::seq (rest change-seq)}
453 | (when last-change-seq {::last-update-seq last-change-seq})
454 | (when-not change {::state :stopped}))
455 | change))
456 | :stopped (-> config ::http-resp :request :http-req .abort))))
457 |
458 | (defn changes-running?
459 | "Returns true only if the given change agent has been started
460 | (using `start-changes`) and is delivering changes to
461 | attached watches."
462 | [^clojure.lang.Agent change-agent]
463 | (boolean (-> change-agent meta ::state #{:init :running})))
464 |
465 | (defn start-changes
466 | "Starts the flow of changes through the given change-agent.
467 | All of the options provided to `change-agent` are used to
468 | configure the underlying _changes feed."
469 | [change-agent]
470 | (send-off change-agent #'run-changes))
471 |
472 | (defn stop-changes
473 | "Stops the flow of changes through the given change-agent.
474 | Change agents can be restarted with `start-changes`."
475 | [change-agent]
476 | (swap! (-> change-agent meta ::changes-config) assoc ::state :stopped)
477 | change-agent)
478 |
479 |
480 | (defmacro with-db
481 | "Takes a URL, database name (useful for localhost only), or an instance of
482 | com.ashafa.clutch.utils.URL. That value is used to configure the subject
483 | of all of the operations within the dynamic scope of body of code."
484 | [database & body]
485 | `(binding [*database* (utils/url ~database)]
486 | ~@body))
487 |
488 | ;;;; CouchDB deftype
489 |
490 | (defprotocol CouchOps
491 | "Defines side-effecting operations on a CouchDB database.
492 | All operations return the CouchDB database reference —
493 | with the return value from the underlying clutch function
494 | added to its :result metadata — for easy threading and
495 | reduction usage.
496 | (EXPERIMENTAL!)"
497 | (create! [this] "Ensures that the database exists, and returns the database's meta info.")
498 | (conj! [this document]
499 | "PUTs a document into CouchDB. Accepts either a document map (using an :_id value
500 | if present as the document id), or a vector/map entry consisting of a
501 | [id document-map] pair.")
502 | (assoc! [this id document]
503 | "PUTs a document into CouchDB. Equivalent to (conj! couch [id document]).")
504 | (dissoc! [this id-or-doc]
505 | "DELETEs a document from CouchDB. Uses a given document map's :_id and :_rev
506 | if provided; alternatively, if passed a string, will blindly attempt to
507 | delete the current revision of the corresponding document."))
508 |
509 | (defn- with-result-meta
510 | [couch result]
511 | (vary-meta couch assoc :result result))
512 |
513 | (deftype CouchDB [url meta]
514 | com.ashafa.clutch.CouchOps
515 | (create! [this] (with-result-meta this (get-database url)))
516 | (conj! [this doc]
517 | (let [[id doc] (cond
518 | (map? doc) [(:_id doc) doc]
519 | (or (vector? doc) (instance? java.util.Map$Entry)) doc)]
520 | (->> (put-document url doc :id id)
521 | (fail-on-404 url)
522 | (with-result-meta this))))
523 | (assoc! [this id document] (conj! this [id document]))
524 | (dissoc! [this id]
525 | (if-let [d (if (document? id)
526 | id
527 | (this id))]
528 | (with-result-meta this (delete-document url d))
529 | (with-result-meta this nil)))
530 |
531 | clojure.lang.ILookup
532 | (valAt [this k] (get-document url k))
533 | (valAt [this k default] (or (.valAt this k) default))
534 |
535 | clojure.lang.Counted
536 | (count [this] (->> (database-info url) (fail-on-404 url) :doc_count))
537 |
538 | clojure.lang.Seqable
539 | (seq [this]
540 | (->> (all-documents url {:include_docs true})
541 | (map :doc)
542 | (map #(clojure.lang.MapEntry. (:_id %) %))))
543 |
544 | clojure.lang.IFn
545 | (invoke [this key] (.valAt this key))
546 | (invoke [this key default] (.valAt this key default))
547 |
548 | clojure.lang.IMeta
549 | (meta [this] meta)
550 | clojure.lang.IObj
551 | (withMeta [this meta] (CouchDB. url meta)))
552 |
553 | (def ^{:private true} couchdb-class CouchDB)
554 |
555 | (defn couch
556 | "Returns an instance of an implementation of CouchOps.
557 | (EXPERIMENTAL!)"
558 | ([url] (CouchDB. url nil))
559 | ([url meta] (CouchDB. url meta)))
560 |
--------------------------------------------------------------------------------
/src/com/ashafa/clutch/cljs_views.clj:
--------------------------------------------------------------------------------
1 | (ns com.ashafa.clutch.cljs-views
2 | (:require
3 | [com.ashafa.clutch :as clutch]
4 | [cljs.closure :as closure]))
5 |
6 | (defn- expand-anon-fn
7 | "Compiles a single anonymous function body in a dummy namespace, with a gensym'ed
8 | name. Separate from the general case because cljs doesn't include cljs.core and
9 | the goog stuff if a namespace is not specified, and advanced gClosure
10 | optimization drops top-level anonymous fns."
11 | [fnbody]
12 | (when-not (and (seq fnbody)
13 | ('#{fn fn*} (first fnbody)))
14 | (throw (IllegalArgumentException. "Simple ClojureScript views must be an anonymous fn, e.g. (fn [doc] …) or #(...)")))
15 | (let [namespace (gensym)
16 | name (with-meta (gensym) {:export true})]
17 | [{:main (symbol (str namespace) (str name))}
18 | [(list 'ns namespace)
19 | (list 'def name fnbody)]]))
20 |
21 | (defn- view*
22 | [options body]
23 | (let [[options' body] (if (and (list? body) ('#{fn fn*} (first body)))
24 | (expand-anon-fn body)
25 | [nil (vec body)])
26 | options (merge {:optimizations :advanced :pretty-print false}
27 | options'
28 | options)]
29 | (when-not (:main options)
30 | (throw (IllegalArgumentException. "Must specify a fully-qualified entry point fn via :main option")))
31 | (str (closure/build body options)
32 | "return " (-> options :main namespace) \. (-> options :main name))))
33 |
34 | (defn- closure
35 | "Wraps the provided string of code in a closure. This isn't strictly needed right now
36 | (i.e. circa couchdb ~1.0.2), but couchdb 1.2 will begin to require that view/filter/validation
37 | code be defined in a single _expression_. This is necessary to ensure that we're producing a
38 | single expression, given Google Closure's penchant for lifting
39 | closures to the top level of advanced-optimized code (even if the code provided to it is entirely
40 | contained within a closure itself, making lifting somewhat pointless AFAICT)."
41 | [code]
42 | (str "(function () {"
43 | code
44 | "})()"))
45 |
46 | (def view
47 | "Compiles a body of ClojureScript into a single Javascript expression, suitable for use in a
48 | CouchDB view. First argument may be a map of options; remaining arguments must be either
49 | (a) a single anonymous function, or (b) a series of top-level ClojureScript forms, starting
50 | with an `ns` declaration. If (b), the map of options must include a `:main` entry to identify
51 | the \"entry point\" for the CouchDB view/filter/validator/etc.
52 |
53 | Contrived examples:
54 |
55 | (view nil '(fn [doc]
56 | (js/emit (aget doc \"_id\") nil)))
57 |
58 | (view {:main 'some-view/main}
59 | '(ns some-view)
60 | '(defn date-components [date]
61 | (-> (re-seq #\"(\\d{4})-(\\d{2})-(\\d{2})\" date)
62 | first
63 | rest))
64 | '(defn main [doc]
65 | (js/emit (apply array (-> doc (aget \"date\") date-components)) nil)))
66 |
67 | If using clutch, you should never have to touch this function. It is registered with clutch
68 | as a view-transformer; just use the view-server-fns macro, indicating a view server language of
69 | :cljs.
70 |
71 | You can also include ClojureScript/Google Closure compiler options in the options map, e.g.
72 | :optimizations, :pretty-print, etc. These options default to :advanced compilation, no
73 | pretty-printing."
74 | (comp closure view*))
75 |
76 | (defmethod clutch/view-transformer :cljs
77 | [_]
78 | {:language :javascript
79 | :compiler (fn [options]
80 | (partial view options))})
81 |
--------------------------------------------------------------------------------
/src/com/ashafa/clutch/http_client.clj:
--------------------------------------------------------------------------------
1 | (ns com.ashafa.clutch.http-client
2 | (:require [clj-http.client :as http]
3 | [cheshire.core :as json]
4 |
5 | [clojure.java.io :as io]
6 | [clojure.string :as str]
7 | [com.ashafa.clutch.utils :as utils])
8 | (:use [cemerick.url :only (url)]
9 | [slingshot.slingshot :only (throw+ try+)])
10 | (:import (java.io IOException InputStream InputStreamReader PushbackReader)
11 | (java.net URL URLConnection HttpURLConnection MalformedURLException)))
12 |
13 |
14 | (def ^{:private true} version "0.3.0")
15 | (def ^{:private true} user-agent (str "com.ashafa.clutch/" version))
16 | (def ^{:dynamic true} *default-data-type* "application/json")
17 | (def ^{:dynamic true} *configuration-defaults* {:socket-timeout 0
18 | :conn-timeout 5000
19 | :follow-redirects true
20 | :save-request? true
21 | :as :json})
22 |
23 | (def ^{:doc "When thread-bound to any value, will be reset! to the
24 | complete HTTP response of the last couchdb request."
25 | :dynamic true}
26 | *response* nil)
27 |
28 | (defmacro fail-on-404
29 | [db expr]
30 | `(let [f# #(let [resp# ~expr]
31 | (if (= 404 (:status *response*))
32 | (throw (IllegalStateException. (format "Database %s does not exist" ~db)))
33 | resp#))]
34 | (if (thread-bound? #'*response*)
35 | (f#)
36 | (binding [*response* nil] (f#)))))
37 |
38 | (defn- set!-*response*
39 | [response]
40 | (when (thread-bound? #'*response*) (set! *response* response))
41 | response)
42 |
43 | (defn- connect
44 | [request]
45 | (let [configuration (merge *configuration-defaults* request)
46 | data (:data request)]
47 | (try+
48 | (let [resp (http/request (merge configuration
49 | {:url (str request)}
50 | (when data {:body data})
51 | (when (instance? InputStream data)
52 | {:length (:data-length request)})))]
53 | (set!-*response* resp))
54 | (catch identity ex
55 | (if (map? ex)
56 | (do
57 | (set!-*response* ex)
58 | (when-not (== 404 (:status ex))
59 | (throw+ ex)))
60 | (throw+ ex))))))
61 |
62 | (defn- configure-request
63 | [method url {:keys [data data-length content-type headers]}]
64 | (assoc url
65 | :method method
66 | :data (if (map? data) (json/generate-string data) data)
67 | :data-length data-length
68 | :headers (merge {"Content-Type" (or content-type *default-data-type*)
69 | "User-Agent" user-agent
70 | "Accept" "*/*"}
71 | headers)))
72 |
73 | (defn couchdb-request*
74 | [method url & {:keys [data data-length content-type headers] :as opts}]
75 | (connect (configure-request method url opts)))
76 |
77 | (defn couchdb-request
78 | "Same as couchdb-request*, but returns only the :body of the HTTP response."
79 | [& args]
80 | (:body (apply couchdb-request* args)))
81 |
82 | (defn lazy-view-seq
83 | "Given the body of a view result or _changes feed (should be an InputStream),
84 | returns a lazy seq of each row of data therein.
85 |
86 | header? should be true
87 | when the result is expected to include additional information in an additional
88 | header line, e.g. total_rows, offset, etc. — in other words, header? should
89 | always be true, unless the response-body is from a continuous _changes feed
90 | (which only include data, no header info). This header info is added as
91 | metadata to the returned lazy seq."
92 | [response-body header?]
93 | (let [lines (utils/read-lines response-body)
94 | [lines meta] (if header?
95 | [(rest lines)
96 | (-> (first lines)
97 | (str/replace #",?\"(rows|results)\":\[\s*$" "}") ; TODO this is going to break eventually :-/
98 | (json/parse-string true))]
99 | [lines nil])]
100 | (with-meta (->> lines
101 | (map (fn [^String line]
102 | (when (.startsWith line "{")
103 | (json/parse-string line true))))
104 | (remove nil?))
105 | ;; TODO why are we dissoc'ing :rows here?
106 | (dissoc meta :rows))))
107 |
108 | (defn view-request
109 | "Accepts the same arguments as couchdb-request*, but processes the result assuming that the
110 | requested resource is a view (using lazy-view-seq)."
111 | [method url & opts]
112 | (if-let [response (apply couchdb-request method (assoc url :as :stream) opts)]
113 | (lazy-view-seq response true)
114 | (throw (java.io.IOException. (str "No such view: " url)))))
115 |
--------------------------------------------------------------------------------
/src/com/ashafa/clutch/utils.clj:
--------------------------------------------------------------------------------
1 | (ns com.ashafa.clutch.utils
2 | (:require [clojure.java.io :as io]
3 | [cemerick.url :as url])
4 | (:import java.net.URLEncoder
5 | java.lang.Class
6 | [java.io File]))
7 |
8 | (defn url
9 | "Thin layer on top of cemerick.url/url that defaults otherwise unqualified
10 | database urls to use `http://localhost:5984` and url-encodes each URL part
11 | provided."
12 | [& [base & parts :as args]]
13 | (try
14 | (apply url/url base (map (comp url/url-encode str) parts))
15 | (catch java.net.MalformedURLException e
16 | (apply url/url "http://localhost:5984" (map (comp url/url-encode str) args)))))
17 |
18 | (defn server-url
19 | [db]
20 | (assoc db :path nil :query nil))
21 |
22 | (defn get-mime-type
23 | [^File file]
24 | (java.net.URLConnection/guessContentTypeFromName (.getName file)))
25 |
26 | ;; TODO should be replaced with a java.io.Closeable Seq implementation and used
27 | ;; in conjunction with with-open on the client side
28 | (defn read-lines
29 | "Like clojure.core/line-seq but opens f with reader. Automatically
30 | closes the reader AFTER YOU CONSUME THE ENTIRE SEQUENCE.
31 |
32 | Pulled from clojure.contrib.io so as to avoid dependency on the old io
33 | namespace."
34 | [f]
35 | (let [read-line (fn this [^java.io.BufferedReader rdr]
36 | (lazy-seq
37 | (if-let [line (.readLine rdr)]
38 | (cons line (this rdr))
39 | (.close rdr))))]
40 | (read-line (io/reader f))))
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/com/ashafa/clutch/view_server.clj:
--------------------------------------------------------------------------------
1 | (ns com.ashafa.clutch.view-server
2 | (:require [cheshire.core :as json]))
3 |
4 | (defn view-server-exec-string
5 | "Generates a string that *should* work to configure a clutch view server
6 | within your local CouchDB instance based on the current process'
7 | java.class.path system property. Assumes that `java` is on CouchDB's
8 | PATH."
9 | []
10 | (format "java -cp \"%s\" clojure.main -i @/com/ashafa/clutch/view_server.clj -e \"(com.ashafa.clutch.view-server/-main)\""
11 | (System/getProperty "java.class.path")))
12 |
13 | (def functions (ref []))
14 |
15 | (def cached-views (ref {}))
16 |
17 | (defn log
18 | [message]
19 | ["log" message])
20 |
21 | (defn reset
22 | [_]
23 | (dosync (ref-set functions []))
24 | true)
25 |
26 | (defn add-function
27 | [[function-string]]
28 | (let [function (load-string function-string)]
29 | (if (fn? function)
30 | (dosync
31 | (alter functions conj function) true)
32 | ["error" "map_compilation_error" "Argument did not evaluate to a function."])))
33 |
34 | (defn map-document
35 | [[document]]
36 | (for [f @functions]
37 | (let [results (try (f document)
38 | (catch Exception error nil))]
39 | (vec results))))
40 |
41 | (defn reduce-values
42 | ([[function-string & arguments-array :as entire-command]]
43 | (reduce-values entire-command false))
44 | ([[function-string & arguments-array] rereduce?]
45 | (try
46 | (let [arguments (first arguments-array)
47 | argument-count (count arguments)
48 | reduce-functions (map #(load-string %) function-string)
49 | [keys values] (if rereduce?
50 | [nil arguments]
51 | (if (> argument-count 1)
52 | (partition argument-count (apply interleave arguments))
53 | [[(ffirst arguments)] [(second (first arguments))]]))]
54 | [true (reduce #(conj %1 (%2 keys values rereduce?)) [] reduce-functions)])
55 | (catch Exception error
56 | ["error" "reduce_compilation_error" (.getMessage error)]))))
57 |
58 | (defn rereduce-values
59 | [command]
60 | (reduce-values command true))
61 |
62 | (defn filter-changes
63 | [func ddoc [ddocs request]]
64 | [true (vec (map #(or (and (func % request) true) false) ddocs))])
65 |
66 | (defn update-changes
67 | [func ddoc args]
68 | (if-let [[ddoc response] (apply func args)]
69 | ["up" ddoc (if (string? response) {:body response} response)]
70 | ["up" ddoc {}]))
71 |
72 | (def ddoc-handlers {"filters" filter-changes
73 | "updates" update-changes})
74 |
75 | (defn ddoc
76 | [arguments]
77 | (let [ddoc-id (first arguments)]
78 | (if (= "new" ddoc-id)
79 | (let [ddoc-id (second arguments)]
80 | (dosync
81 | (alter cached-views assoc ddoc-id (nth arguments 2)))
82 | true)
83 | (if-let [ddoc (@cached-views ddoc-id)]
84 | (let [func-path (map keyword (second arguments))
85 | command (first (second arguments))
86 | func-args (nth arguments 2)]
87 | (if-let [handle (ddoc-handlers command)]
88 | (let [func-key (last func-path)
89 | cfunc (get-in ddoc func-path)
90 | func (if-not (fn? cfunc) (load-string cfunc) cfunc)]
91 | (if-not (fn? cfunc)
92 | (dosync
93 | (alter cached-views assoc-in [ddoc-id func-key] func)))
94 | (apply handle [func (@cached-views ddoc-id) func-args]))
95 | ["error" "unknown_command" (str "Unknown ddoc '" command "' command")]))
96 | ["error" "query_protocol_error" (str "Uncached design doc id: '" ddoc-id "'")]))))
97 |
98 | (def handlers {"log" log
99 | "ddoc" ddoc
100 | "reset" reset
101 | "add_fun" add-function
102 | "map_doc" map-document
103 | "reduce" reduce-values
104 | "rereduce" rereduce-values})
105 |
106 | (defn run
107 | []
108 | (try
109 | (flush)
110 | (when-let [line (read-line)] ; don't throw an error if we just get EOF
111 | (let [input (json/parse-string line true)
112 | command (first input)
113 | handle (handlers command)
114 | return-str (if handle
115 | (handle (next input))
116 | ["error" "unknown_command" (str "No '" command "' command.")])]
117 | (println (json/generate-string return-str))))
118 | (catch Exception e
119 | (println (json/generate-string
120 | ["fatal" "fatal_error" (let [w (java.io.StringWriter.)]
121 | (.printStackTrace e (java.io.PrintWriter. w))
122 | (.toString w))]))
123 | (System/exit 1)))
124 | (recur))
125 |
126 |
127 | (defn -main
128 | [& args]
129 | (run))
130 |
--------------------------------------------------------------------------------
/test/clojure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clojure-clutch/clutch/62d5ae95b975e3bd072d83b55dfea540cb7e19f8/test/clojure.png
--------------------------------------------------------------------------------
/test/clutch/test/changes.clj:
--------------------------------------------------------------------------------
1 | (ns clutch.test.changes
2 | (:use com.ashafa.clutch
3 | [test-clutch :only (defdbtest test-docs test-database-name)]
4 | clojure.test)
5 | (:refer-clojure :exclude (conj! assoc! dissoc!)))
6 |
7 | (defn- wait-for-condition
8 | [f desc]
9 | (let [wait-condition (loop [waiting 0]
10 | (cond
11 | (f) true
12 | (>= waiting 10) false
13 | :else (do
14 | (Thread/sleep 1000)
15 | (recur (inc waiting)))))]
16 | (when-not (is wait-condition desc)
17 | (throw (IllegalStateException. desc)))
18 | wait-condition))
19 |
20 | (deftest simple-agent
21 | (let [db (get-database (test-database-name "create-type"))]
22 | (with-db db
23 | (try
24 | (let [a (change-agent)
25 | updates (atom [])]
26 | (add-watch a :watcher (fn [_ _ _ x] (when x (swap! updates conj x))))
27 | (start-changes a)
28 | (bulk-update (map #(hash-map :_id (str %)) (range 4)))
29 | (delete-document (get-document "2"))
30 | (update-document (assoc (get-document "1") :assoc :a 5))
31 | (delete-database)
32 | (wait-for-condition #(-> @updates last :last_seq) "Updates not received")
33 | (is (= [{:id "0"} {:id "1"} {:id "2"} {:id "3"} {:id "2" :deleted true} {:id "1"}]
34 | (->> @updates
35 | drop-last
36 | (map #(dissoc % :changes :seq))))))
37 | (finally
38 | (delete-database))))))
39 |
40 | (defdbtest can-stop-change-agent
41 | (let [a (change-agent)
42 | updates (atom [])]
43 | (add-watch a :watcher (fn [_ _ _ x] (when x (swap! updates conj x))))
44 | (start-changes a)
45 | (bulk-update (take 3 test-docs))
46 | (wait-for-condition #(= 3 (count @updates)) "3 updates not received")
47 | (stop-changes a)
48 | (put-document (last test-docs))
49 | (Thread/sleep 2000)
50 | (is (= 3 (count @updates)))))
51 |
52 | #_(defdbtest restarting-changes
53 | (let [a (change-agent)
54 | updates (atom [])
55 | docs (map #(hash-map :_id (str %)) (range))]
56 | (add-watch a :watcher (fn [_ _ _ x] (println "f" x) (when x (swap! updates conj x))))
57 | (start-changes a)
58 | (bulk-update (take 4 docs))
59 | (wait-for-condition #(= 4 (count @updates)) "4 updates not received")
60 | (stop-changes a)
61 |
62 | (put-document (nth docs 4))
63 | (Thread/sleep 1000)
64 | (is (= 4 (count @updates)))
65 |
66 | (start-changes a)
67 | (wait-for-condition #(= 5 (count @updates)) "1 update not received")
68 |
69 | (bulk-update (->> docs (drop 5) (take 3)))
70 | (wait-for-condition #(= 8 (count @updates)) "3 updates not received")
71 | (is (= 8 (count @updates)))
72 |
73 | (testing "start-changes with :since option"
74 | (println (.getQueueCount a) (-> a meta :com.ashafa.clutch/state))
75 | (stop-changes a)
76 | (reset! updates [])
77 | (println (.getQueueCount a) (-> a meta :com.ashafa.clutch/state))
78 | (start-changes a :since 0)
79 | (put-document (->> docs (drop 8) first))
80 | (Thread/sleep 10000)
81 | (println (.getQueueCount a) (-> a meta :com.ashafa.clutch/state))
82 | (wait-for-condition
83 | #(do (println (count @updates) (map :seq @updates)) (= 9 (count @updates)))
84 | "change agent probably not stopped straight away, still waiting for a change to restart with new params")
85 | (bulk-update (->> docs (drop 9) (take 7)))
86 | (wait-for-condition #(= 16 (count @updates)) "7 updates not received")
87 | (is (= 16 (count @updates))))
88 |
89 | (testing "changes-running? predicate"
90 | (is (changes-running? a))
91 | (is (instance? Boolean (changes-running? a)))
92 | (stop-changes a)
93 | (is (not (changes-running? a)))
94 | (is (instance? Boolean (changes-running? a))))))
95 |
96 | (defdbtest filtered-change-agent
97 | (save-filter "scores"
98 | (view-server-fns :javascript
99 | {:more-than-50 "function (doc, req) { return doc['score'] > 50; }"}))
100 | (let [a (change-agent :filter "scores/more-than-50")
101 | updates (atom [])]
102 | (add-watch a :watcher (fn [_ _ _ x] (when x (swap! updates conj x))))
103 | (start-changes a)
104 | (bulk-update [{:score 22 :_id "x"} {:score 79 :_id "y"}])
105 | (wait-for-condition #(= 1 (count @updates)) "1 update not received")
106 | (is (= 1 (count @updates)))
107 | (is (= {:id "y"} (-> @updates
108 | first
109 | (select-keys [:id]))))))
110 |
111 | (defdbtest parameterized-filtered-change-agent
112 | (save-filter "scores"
113 | (view-server-fns :javascript
114 | {:more-than-x "function (doc, req) { return doc['score'] > req.query.score; }"}))
115 | (let [a (change-agent :filter "scores/more-than-x" :score 25)
116 | updates (atom [])]
117 | (add-watch a :watcher (fn [_ _ _ x] (when x (swap! updates conj x))))
118 | (start-changes a)
119 | (bulk-update [{:score 22 :_id "x"} {:score 79 :_id "y"} {:score 27 :_id "z"}])
120 | (wait-for-condition #(= 2 (count @updates)) "2 update not received")
121 | (is (= 2 (count @updates)))
122 | ;; order-insensitive here because cloudant/bigcouch can yield changes in any order
123 | (is (= #{{:id "y"} {:id "z"}}
124 | (->> @updates
125 | (map #(select-keys % [:id]))
126 | set)))))
127 |
--------------------------------------------------------------------------------
/test/clutch/test/type.clj:
--------------------------------------------------------------------------------
1 | (ns clutch.test.type
2 | (:use clojure.test
3 | com.ashafa.clutch
4 | [test-clutch :only (defdbtest test-database-name test-database-url *test-database*)])
5 | (:refer-clojure :exclude (conj! assoc! dissoc!)))
6 |
7 | (deftest create
8 | (let [name (test-database-url (test-database-name "create-type"))
9 | db (couch name)]
10 | (try
11 | ; :update_seq can change anytime, esp. in cloudant
12 | (is (= (-> db create! meta :result (dissoc :update_seq))
13 | (dissoc (get-database name) :update_seq)))
14 | (finally
15 | (delete-database name)))))
16 |
17 | (defdbtest simple
18 | (let [db (couch *test-database*)]
19 | (reduce conj! db (for [x (range 100)]
20 | {:_id (str x) :a [1 2 x]}))
21 | (= ["0" {:_id "0" :a [1 2 "0"]}]
22 | (-> db first (update-in [1] dissoc-meta)))
23 | (is (= [1 2 68]
24 | (:a (get db "68"))
25 | (-> "68" db :a)))
26 | (is (= 100 (count db)))
27 | (is (= 68
28 | (get-in db ["68" :a 2])
29 | (get-in (into {} db) ["68" :a 2])))
30 | (dissoc! db "68")
31 | (is (nil? (get db "68")))
32 | (is (nil? (db "68")))
33 | (is (= {:a 5 :b 6}
34 | (-> db
35 | (assoc! :foo {:a 5 :b 6})
36 | meta
37 | :result
38 | dissoc-meta)
39 | (dissoc-meta (:foo db))
40 | (dissoc-meta (db :foo))))))
41 |
42 | (deftest use-type-as-db-arg
43 | (let [name (get-database (test-database-name "use-type-as-db-arg"))
44 | db (couch name)]
45 | (try
46 | (dotimes [x 100]
47 | (put-document db {:a x :_id (str x)}))
48 | (bulk-update db (for [x (range 100)] {:_id (str "x" x) :x x}))
49 | (is (= 200 (:doc_count (database-info db))))
50 | (finally (delete-database name)))))
--------------------------------------------------------------------------------
/test/clutch/test/views.clj:
--------------------------------------------------------------------------------
1 | (ns clutch.test.views
2 | (:require (com.ashafa.clutch
3 | [view-server :as view-server]
4 | [utils :as utils]))
5 | (:use clojure.test
6 | com.ashafa.clutch
7 | [test-clutch :only (defdbtest test-database-name test-host
8 | test-docs test-database-url *test-database*)])
9 | (:refer-clojure :exclude (conj! assoc! dissoc!)))
10 |
11 | (declare ^{:dynamic true} *clj-view-svr-config*)
12 |
13 | ; don't squash existing canonical "clojure" view server config
14 | (def ^{:private true} view-server-name :clutch-test)
15 |
16 | (.addMethod view-transformer
17 | view-server-name
18 | (get-method view-transformer :clojure))
19 |
20 | (use-fixtures
21 | :once
22 | #(binding [*clj-view-svr-config* (try
23 | (when (re-find #"localhost" test-host)
24 | (configure-view-server (utils/url test-host)
25 | (view-server/view-server-exec-string)
26 | :language view-server-name))
27 | (catch java.io.IOException e (.printStackTrace e)))]
28 | (when-not *clj-view-svr-config*
29 | (println "Could not autoconfigure clutch view server,"
30 | "skipping tests that depend upon it!")
31 | (println "(This is normal if you're testing against e.g. Cloudant)")
32 | (println (view-server/view-server-exec-string))
33 | (println))
34 | (%)))
35 |
36 | (defdbtest lazy-view-results
37 | (bulk-update (map (comp (partial hash-map :_id) str) (range 50000)))
38 | (is (= {:total_rows 50000 :offset 0} (meta (all-documents))))
39 | (let [time #(let [t (System/currentTimeMillis)]
40 | [(%) (- (System/currentTimeMillis) t)])
41 | [f tf] (time #(first (all-documents)))
42 | [l tl] (time #(last (all-documents)))]
43 | ; any other way to check that the returned seq is properly lazy?
44 | (is (< 10 (/ tl tf)))))
45 |
46 | (defdbtest create-a-design-view
47 | (when *clj-view-svr-config*
48 | (let [view-document (save-view "users"
49 | (view-server-fns view-server-name
50 | {:names-with-score-over-70
51 | {:map #(if (> (:score %) 70) [[nil (:name %)]])}}))]
52 | (is (map? (-> (get-document (view-document :_id)) :views :names-with-score-over-70))))))
53 |
54 | (defdbtest use-a-design-view-with-spaces-in-key
55 | (when *clj-view-svr-config*
56 | (bulk-update test-docs)
57 | (save-view "users"
58 | (view-server-fns view-server-name
59 | {:names-and-scores
60 | {:map (fn [doc] [[(:name doc) (:score doc)]])}}))
61 | (is (= [98]
62 | (map :value (get-view "users" :names-and-scores {:key "Jane Thompson"}))))))
63 |
64 | (defdbtest use-a-design-view-with-map-only
65 | (when *clj-view-svr-config*
66 | (bulk-update test-docs)
67 | (save-view "users"
68 | (view-server-fns view-server-name
69 | {:names-with-score-over-70-sorted-by-score
70 | {:map #(if (> (:score %) 70) [[(:score %) (:name %)]])}}))
71 | (is (= ["Robert Jones" "Jane Thompson"]
72 | (map :value (get-view "users" :names-with-score-over-70-sorted-by-score))))
73 | (put-document {:name "Test User 1" :score 55})
74 | (put-document {:name "Test User 2" :score 78})
75 | (is (= ["Test User 2" "Robert Jones" "Jane Thompson"]
76 | (map :value (get-view "users" :names-with-score-over-70-sorted-by-score))))
77 | (save-view "users"
78 | (view-server-fns view-server-name
79 | {:names-with-score-less-than-70-sorted-by-name
80 | {:map #(if (< (:score %) 70) [[(:name %) (:name %)]])}}))
81 | (is (= ["John Smith" "Sarah Parker" "Test User 1"]
82 | (map :value (get-view "users" :names-with-score-less-than-70-sorted-by-name))))))
83 |
84 | (defdbtest use-a-design-view-with-post-keys
85 | (when *clj-view-svr-config*
86 | (bulk-update test-docs)
87 | (put-document {:name "Test User 1" :score 18})
88 | (put-document {:name "Test User 2" :score 7})
89 | (save-view "users"
90 | (view-server-fns view-server-name
91 | {:names-keyed-by-scores
92 | {:map #(cond (< (:score %) 30) [[:low (:name %)]]
93 | (< (:score %) 70) [[:medium (:name %)]]
94 | :else [[:high (:name %)]])}}))
95 | (is (= #{"Sarah Parker" "John Smith" "Test User 1" "Test User 2"}
96 | (->> (get-view "users" :names-keyed-by-scores {} {:keys [:medium :low]})
97 | (map :value)
98 | set)))))
99 |
100 | (defdbtest use-a-design-view-with-both-map-and-reduce
101 | (when *clj-view-svr-config*
102 | (bulk-update test-docs)
103 | (save-view "scores"
104 | (view-server-fns view-server-name
105 | {:sum-of-all-scores
106 | {:map (fn [doc] [[nil (:score doc)]])
107 | :reduce (fn [keys values _] (apply + values))}}))
108 | (is (= 302 (-> (get-view "scores" :sum-of-all-scores) first :value)))
109 | (put-document {:score 55})
110 | (is (= 357 (-> (get-view "scores" :sum-of-all-scores) first :value)))))
111 |
112 | (defdbtest use-a-design-view-with-multiple-emits
113 | (when *clj-view-svr-config*
114 | (put-document {:players ["Test User 1" "Test User 2" "Test User 3"]})
115 | (put-document {:players ["Test User 4"]})
116 | (put-document {:players []})
117 | (put-document {:players ["Test User 5" "Test User 6" "Test User 7" "Test User 8"]})
118 | (save-view "count"
119 | (view-server-fns view-server-name
120 | {:number-of-players
121 | {:map (fn [doc] (map (fn [d] [d 1]) (:players doc)))
122 | :reduce (fn [keys values _] (reduce + values))}}))
123 | (is (= 8 (-> (get-view "count" :number-of-players) first :value)))))
124 |
125 | (defdbtest use-ad-hoc-view
126 | (when *clj-view-svr-config*
127 | (bulk-update test-docs)
128 | (let [view (ad-hoc-view
129 | (view-server-fns view-server-name
130 | {:map (fn [doc] (if (re-find #"example\.com$" (:email doc))
131 | [[nil (:email doc)]]))}))]
132 | (is (= #{"robert.jones@example.com" "sarah.parker@example.com"}
133 | (set (map :value view)))))))
134 |
135 | (defdbtest use-ad-hoc-view-with-javascript-view-server
136 | (if (re-find #"cloudant" test-host)
137 | (println "skipping ad-hoc view test; not supported by Cloudant")
138 | (do
139 | (bulk-update test-docs)
140 | (let [view (ad-hoc-view
141 | (view-server-fns :javascript
142 | {:map "function(doc){if(doc.email.indexOf('test.com')>0)emit(null,doc.email);}"}))]
143 | (is (= #{"john.smith@test.com" "jane.thompson@test.com"}
144 | (set (map :value view))))))))
145 |
146 | ;; TODO this sucks; can we get leiningen to just not test certain namespaces, and keep the
147 | ;; cljs view tests there to avoid this conditional and the eval junk?!
148 | (if (neg? (compare (clojure-version) "1.4.0"))
149 | (deftest no-cljs-error
150 | (is (thrown-with-msg? UnsupportedOperationException #"Could not load com.ashafa.clutch.cljs-views"
151 | (view-transformer :cljs))))
152 | ;; need the eval to work around view-server-fns macroexpansion in 1.2, which will end
153 | ;; up calling (view-transformer :cljs)
154 | (eval '(let [cljs-view-result [{:id "x", :key ["x" 0], :value 1}
155 | {:id "x", :key ["x" 1], :value 2}
156 | {:id "y", :key ["y" 0], :value 1}
157 | {:id "y", :key ["y" 1], :value 2}
158 | {:id "y", :key ["y" 2], :value 3}]]
159 | (defdbtest cljs-simple
160 | (bulk-update [{:_id "x" :count 2}
161 | {:_id "y" :count 3}])
162 | (save-view "cljs-views"
163 | (view-server-fns :cljs
164 | {:enumeration {:map #(dotimes [x (aget % "count")]
165 | (js/emit (js/Array (aget % "_id") x) (inc x)))}}))
166 | (is (= cljs-view-result
167 | (get-view "cljs-views" :enumeration))))
168 |
169 | (defdbtest cljs-inline-namespace
170 | (bulk-update [{:_id "x" :count 2}
171 | {:_id "y" :count 3}])
172 | (save-view "namespaced-cljs-views"
173 | (view-server-fns {:language :cljs
174 | :main 'inline.namespace.couchview/main}
175 | {:enumeration {:map [(ns inline.namespace.couchview)
176 | (defn view-key
177 | [doc x]
178 | (js/Array (aget doc "_id") x))
179 | (defn ^:export main
180 | [doc]
181 | (dotimes [x (aget doc "count")]
182 | (js/emit (view-key doc x) (inc x))))]}}))
183 | (is (= cljs-view-result
184 | (get-view "namespaced-cljs-views" :enumeration))))
185 |
186 | (defdbtest cljs-require
187 | (bulk-update [{:_id "x" :count 2}
188 | {:_id "y" :count 3}])
189 | (save-view "cljs-views-require"
190 | (view-server-fns {:language :cljs
191 | :main 'inline.namespace.couchview/main}
192 | {:enumeration {:map [(ns inline.namespace.couchview
193 | (:require [clutch.test.views.util :as util]))
194 | (defn ^:export main
195 | [doc]
196 | (doseq [key (util/enumerate-count doc)]
197 | (js/emit (to-array key) true)))]}}))
198 | (is (= (map #(assoc % :value true) cljs-view-result)
199 | (get-view "cljs-views-require" :enumeration)))))))
200 |
201 |
--------------------------------------------------------------------------------
/test/clutch/test/views/util.cljs:
--------------------------------------------------------------------------------
1 | (ns clutch.test.views.util)
2 |
3 | (defn enumerate-count
4 | "A trivial fn that is used by one of the ClojureScript view tests to ensure that
5 | requires and such work as expected."
6 | [doc]
7 | (->> (aget doc "count")
8 | range
9 | (map vector (repeat (aget doc "_id")))))
--------------------------------------------------------------------------------
/test/couchdb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clojure-clutch/clutch/62d5ae95b975e3bd072d83b55dfea540cb7e19f8/test/couchdb.png
--------------------------------------------------------------------------------
/test/test_clutch.clj:
--------------------------------------------------------------------------------
1 | (ns ^{:author "Tunde Ashafa"}
2 | test-clutch
3 | (:require (com.ashafa.clutch
4 | [http-client :as http-client]
5 | [utils :as utils])
6 | [clojure.string :as str]
7 | [clojure.java.io :as io])
8 | (:use com.ashafa.clutch
9 | [cemerick.url :only (url)]
10 | [slingshot.slingshot :only (throw+ try+)]
11 | clojure.set
12 | clojure.test)
13 | (:import (java.io File ByteArrayInputStream FileInputStream ByteArrayOutputStream)
14 | (java.net URL))
15 | (:refer-clojure :exclude (conj! assoc! dissoc!)))
16 |
17 | ; Can be e.g.
18 | ; "https://username:password@account.cloudant.com" or
19 | ; (assoc (utils/url "localhost")
20 | ; :username "username"
21 | ; :password "password")
22 | (def test-host (or (System/getenv "clutch_url") "http://localhost:5984"))
23 |
24 | (println "Testing using Clojure" *clojure-version*
25 | "on Java" (System/getProperty "java.version")
26 | "=>>" (-> test-host url (assoc :username nil :password nil) str))
27 | (println "CouchDB server info:" (couchdb-info (-> test-host url str)))
28 |
29 | (def resources-path "test")
30 |
31 | (def test-docs [{:name "John Smith"
32 | :email "john.smith@test.com"
33 | :score 65}
34 | {:name "Jane Thompson"
35 | :email "jane.thompson@test.com"
36 | :score 98}
37 | {:name "Robert Jones"
38 | :email "robert.jones@example.com"
39 | :score 80}
40 | {:name "Sarah Parker"
41 | :email "sarah.parker@example.com"
42 | :score 59}])
43 |
44 | (def ^{:private true} to-byte-array @#'com.ashafa.clutch/to-byte-array)
45 |
46 | (declare ^{:dynamic true} *test-database*)
47 |
48 | (defn test-database-name
49 | [test-name]
50 | (str "test-db-" (str/replace (str test-name) #"[^$]+\$([^@]+)@.*" "$1")))
51 |
52 | (defn test-database-url
53 | [db-name]
54 | (utils/url (utils/url test-host) db-name))
55 |
56 | (defmacro defdbtest [name & body]
57 | `(deftest ~name
58 | (binding [*test-database* (get-database (test-database-url (test-database-name ~name)))]
59 | (try
60 | (with-db *test-database* ~@body)
61 | (finally
62 | (delete-database *test-database*))))))
63 |
64 | (deftest check-couchdb-connection
65 | (is (= "Welcome" (:couchdb (couchdb-info (test-database-url nil))))))
66 |
67 | (deftest get-list-check-and-delete-database
68 | (let [name "clutch_test_db"
69 | url (test-database-url name)
70 | *test-database* (get-database url)]
71 | (is (= name (:db_name *test-database*)))
72 | (is ((set (all-databases url)) name))
73 | (is (= name (:db_name (database-info url))))
74 | (is (:ok (delete-database url)))
75 | (is (nil? ((set (all-databases url)) name)))))
76 |
77 | (deftest database-name-escaping
78 | (let [name (test-database-name "foo_$()+-/bar")
79 | url (test-database-url name)]
80 | (try
81 | (let [dbinfo (get-database url)]
82 | (is (= name (:db_name dbinfo))))
83 | (put-document url {:_id "a" :b 0})
84 | (is (= 0 (:b (get-document url "a"))))
85 | (delete-document url (get-document url "a"))
86 | (is (nil? (get-document url "a")))
87 | (finally
88 | (delete-database url)))))
89 |
90 | (defn- valid-id-charcode?
91 | [code]
92 | (cond
93 | ; c.c.json doesn't cope well with the responses provided when CR or LF are included
94 | (#{10 13} code) false
95 | ; D800–DFFF only used in surrogate pairs, invalid anywhere else (couch chokes on them)
96 | (and (>= code 0xd800) (<= code 0xdfff)) false
97 | (> code @#'com.ashafa.clutch/highest-supported-charcode) false
98 | :else true))
99 |
100 | ; create a document containing each of the 65K chars in unicode BMP.
101 | ; this ensures that utils/id-encode is doing what it should and that we aren't screwing up
102 | ; encoding issues generally (which are easy regressions to introduce)
103 | (defdbtest test-docid-encoding
104 | ; doing a lot of requests here -- the test is crazy-slow if delayed_commit=false,
105 | ; so let's use the iron we've got
106 | (doseq [x (range 0xffff)
107 | :when (valid-id-charcode? x)
108 | :let [id (str "a" (char x)) ; doc ids can't start with _, so prefix everything
109 | id-desc (str x " " id)]]
110 | (try
111 | (is (= id (:_id (put-document {:_id id}))) id-desc)
112 | (let [doc (get-document id)]
113 | (is (= {} (dissoc-meta doc)))
114 | (delete-document doc)
115 | (is (nil? (get-document id))))
116 | (catch Exception e
117 | (is false (str "Error for " id-desc ": " (.getMessage e)))))))
118 |
119 | (defdbtest create-a-document
120 | (let [document (put-document (first test-docs))]
121 | (are [k] (contains? document k)
122 | :_id :_rev)))
123 |
124 | (defdbtest create-a-document-with-id
125 | (let [document (put-document (first test-docs) :id "my_id")]
126 | (is (= "my_id" (document :_id)))))
127 |
128 | (defdbtest test-exists
129 | (put-document {:_id "foo" :a 5})
130 | (is (not (document-exists? "bar")))
131 | (is (document-exists? "foo")))
132 |
133 | (defrecord Foo [a])
134 |
135 | (defdbtest create-with-record
136 | (let [rec (put-document (Foo. "bar") :id "docid")]
137 | (is (instance? Foo rec)))
138 | (is (= "bar" (-> "docid" get-document :a))))
139 |
140 | (defdbtest get-a-document
141 | (let [created-document (put-document (nth test-docs 2))
142 | fetched-document (get-document (created-document :_id))]
143 | (are [x y z] (= x y z)
144 | "Robert Jones" (created-document :name) (fetched-document :name)
145 | "robert.jones@example.com" (created-document :email) (fetched-document :email)
146 | 80 (created-document :score) (fetched-document :score))))
147 |
148 | (defdbtest get-a-document-revision
149 | (let [created-document (put-document (nth test-docs 2))
150 | updated-doc (update-document (assoc created-document :newentry 1))
151 | fetched-document (get-document (:_id created-document)
152 | :rev (:_rev created-document))]
153 | (are [x y z] (= x y z)
154 | "Robert Jones" (created-document :name) (fetched-document :name)
155 | "robert.jones@example.com" (created-document :email) (fetched-document :email)
156 | 80 (:score created-document) (:score fetched-document)
157 | nil (:newentry created-document) (:newentry fetched-document))
158 | (is (= 1 (:newentry updated-doc)))))
159 |
160 | (defmacro failing-request
161 | [expected-status & body]
162 | `(binding [http-client/*response* nil]
163 | (try+
164 | ~@body
165 | (assert false)
166 | (catch identity ex#
167 | (is (== ~expected-status (:status ex#)))
168 | (is (map? http-client/*response*))))))
169 |
170 | (defdbtest verify-response-code-access
171 | (put-document (first test-docs) :id "some_id")
172 | (failing-request 409 (put-document (first test-docs) :id "some_id")))
173 |
174 | (defdbtest update-a-document
175 | (let [id (:_id (put-document (nth test-docs 3)))]
176 | (update-document (get-document id) {:email "test@example.com"})
177 | (is (= "test@example.com" (:email (get-document id)))))
178 | (testing "no update map or fn"
179 | (let [id (:_id (put-document (nth test-docs 3)))]
180 | (update-document (merge (get-document id) {:email "test@example.com"}))
181 | (is (= "test@example.com" (:email (get-document id)))))))
182 |
183 | (defdbtest update-a-document-with-a-function
184 | (let [id (:_id (put-document (nth test-docs 2)))]
185 | (update-document (get-document id) update-in [:score] + 3)
186 | (is (= 83 (:score (get-document id))))))
187 |
188 | (defdbtest update-with-updated-map
189 | (-> (nth test-docs 2)
190 | (put-document :id "docid")
191 | (assoc :a "bar")
192 | update-document)
193 | (is (= "bar" (-> "docid" get-document :a))))
194 |
195 | (defdbtest update-with-record
196 | (let [rec (-> (Foo. "bar")
197 | (merge (put-document {} :id "docid"))
198 | update-document)]
199 | (is (instance? Foo rec)))
200 | (is (= "bar" (-> "docid" get-document :a))))
201 |
202 | (defdbtest delete-a-document
203 | (put-document (second test-docs) :id "my_id")
204 | (is (get-document "my_id"))
205 | (is (true? (:ok (delete-document (get-document "my_id")))))
206 | (is (nil? (get-document "my_id"))))
207 |
208 | (defdbtest copy-a-document
209 | (let [doc (put-document (first test-docs) :id "src")]
210 | (copy-document "src" "dst")
211 | (copy-document doc "dst2")
212 | (is (= (dissoc-meta doc)
213 | (-> "dst" get-document dissoc-meta)
214 | (-> "dst2" get-document dissoc-meta)))))
215 |
216 | (defdbtest copy-document-overwrite
217 | (let [doc (put-document (first test-docs) :id "src")
218 | overwrite (put-document (second test-docs) :id "overwrite")]
219 | (copy-document doc overwrite)
220 | (is (= (dissoc-meta doc) (dissoc-meta (get-document "overwrite"))))))
221 |
222 | (defdbtest copy-document-attachments
223 | (let [doc (put-document (first test-docs) :id "src")
224 | file (File. (str resources-path "/couchdb.png"))
225 | doc (put-attachment doc file :filename :image)
226 | doc (-> doc :id get-document)]
227 | (copy-document "src" "dest")
228 | (let [copy (get-document "dest")
229 | copied-attachment (get-attachment copy :image)]
230 | (is (= (dissoc-meta doc) (dissoc-meta copy)))
231 | (is (= (-> file to-byte-array seq)
232 | (-> copied-attachment to-byte-array seq))))))
233 |
234 | (defdbtest copy-document-fail-overwrite
235 | (put-document (first test-docs) :id "src")
236 | (put-document (second test-docs) :id "overwrite")
237 | (failing-request 409 (copy-document "src" "overwrite")))
238 |
239 | (defdbtest get-all-documents-with-query-parameters
240 | (bulk-update test-docs)
241 | (let [all-documents-descending (all-documents {:include_docs true :descending true})
242 | all-documents-ascending (all-documents {:include_docs true :descending false})]
243 | (are [results] (= 4 (:total_rows (meta results)))
244 | all-documents-descending
245 | all-documents-ascending)
246 | (are [name] (= "Sarah Parker" name)
247 | (-> all-documents-descending first :doc :name)
248 | (-> all-documents-ascending last :doc :name))))
249 |
250 | (defdbtest get-all-documents-with-post-keys
251 | (doseq [[n x] (keep-indexed vector test-docs)]
252 | (put-document x :id (str (inc n))))
253 | (let [all-documents (all-documents {:include_docs true} {:keys ["1" "2"]})
254 | all-documents-matching-keys all-documents]
255 | (is (= ["John Smith" "Jane Thompson"]
256 | (map #(-> % :doc :name) all-documents-matching-keys)))
257 | (is (= 4 (:total_rows (meta all-documents))))))
258 |
259 | (defdbtest bulk-update-new-documents
260 | (bulk-update test-docs)
261 | (is (= 4 (:total_rows (meta (all-documents))))))
262 |
263 | (defdbtest bulk-update-documents
264 | (bulk-update test-docs)
265 | (bulk-update (->> (all-documents {:include_docs true})
266 | (map :doc)
267 | (map #(assoc % :updated true))))
268 | (is (every? true? (map #(-> % :doc :updated) (all-documents {:include_docs true})))))
269 |
270 | (defdbtest inline-attachments
271 | (let [clojure-img-file (str resources-path "/clojure.png")
272 | couchdb-img-file (str resources-path "/couchdb.png")
273 | couch-filename :couchdb.png
274 | bytes-filename :couchdbbytes.png
275 | created-document (put-document (nth test-docs 3)
276 | :attachments [clojure-img-file
277 | {:data (to-byte-array (FileInputStream. couchdb-img-file))
278 | :data-length (-> couchdb-img-file File. .length)
279 | :filename bytes-filename :mime-type "image/png"}
280 | {:data (FileInputStream. couchdb-img-file)
281 | :data-length (-> couchdb-img-file File. .length)
282 | :filename couch-filename :mime-type "image/png"}])
283 | fetched-document (get-document (created-document :_id))]
284 | (are [attachment-keys] (= #{:clojure.png couch-filename bytes-filename} attachment-keys)
285 | (set (keys (created-document :_attachments)))
286 | (set (keys (fetched-document :_attachments))))
287 | (are [doc file-key] (= "image/png" (-> doc :_attachments file-key :content_type))
288 | created-document :clojure.png
289 | fetched-document :clojure.png
290 | created-document couch-filename
291 | fetched-document couch-filename
292 | created-document bytes-filename
293 | fetched-document bytes-filename)
294 | (are [path file-key] (= (.length (File. path)) (-> fetched-document :_attachments file-key :length))
295 | clojure-img-file :clojure.png
296 | couchdb-img-file couch-filename
297 | couchdb-img-file bytes-filename)))
298 |
299 | #_(defdbtest standalone-attachments
300 | (let [document (put-document (first test-docs))
301 | path (str resources-path "/couchdb.png")
302 | filename-with-space (keyword "couchdb - image2")
303 | updated-document-meta (put-attachment document path :filename :couchdb-image)
304 | updated-document-meta (put-attachment (assoc document :_rev (:rev updated-document-meta))
305 | (FileInputStream. path)
306 | :filename filename-with-space
307 | :mime-type "image/png"
308 | :data-length (-> path File. .length))
309 | updated-document-meta (put-attachment (assoc document :_rev (:rev updated-document-meta))
310 | (to-byte-array (FileInputStream. path))
311 | :filename :bytes-image :mime-type "image/png"
312 | :data-length (-> path File. .length))
313 |
314 | _ (.println System/out (pr-str (String.
315 | (com.ashafa.clutch.http-client/couchdb-request :get
316 | (-> (cemerick.url/url *test-database* (updated-document-meta :id))
317 | (assoc :query {:attachments true}
318 | :as :byte-array)))
319 | "UTF-8")))
320 | _ (do (flush) (Thread/sleep 5000))
321 | #_#_document-with-attachments (get-document (updated-document-meta :id) :attachments true)]
322 | #_((is (= #{:couchdb-image filename-with-space :bytes-image} (set (keys (:_attachments document-with-attachments)))))
323 | (is (= "image/png" (-> document-with-attachments :_attachments :couchdb-image :content_type)))
324 | (is (contains? (-> document-with-attachments :_attachments :couchdb-image) :data))
325 |
326 | (is (= (-> document-with-attachments :_attachments :couchdb-image (select-keys [:data :content_type :length]))
327 | (-> document-with-attachments :_attachments filename-with-space (select-keys [:data :content_type :length]))
328 | (-> document-with-attachments :_attachments :bytes-image (select-keys [:data :content_type :length]))))
329 |
330 | (is (thrown? IllegalArgumentException (put-attachment document (Object.))))
331 | (is (thrown? IllegalArgumentException (put-attachment document (ByteArrayInputStream. (make-array Byte/TYPE 0))))))))
332 |
333 | (defdbtest stream-attachments
334 | (let [document (put-document (nth test-docs 3))
335 | updated-document-meta (put-attachment document (str resources-path "/couchdb.png")
336 | :filename :couchdb-image
337 | :mime-type "other/mimetype")
338 | document-with-attachments (get-document (updated-document-meta :id))
339 | data (to-byte-array (java.io.File. (str resources-path "/couchdb.png")))]
340 | (is (= "other/mimetype" (-> document-with-attachments :_attachments :couchdb-image :content_type)))
341 | (is (= (seq data) (-> (get-attachment document-with-attachments :couchdb-image) to-byte-array seq)))))
342 |
343 | (deftest replicate-a-database
344 | (let [source (url test-host "source_test_db")
345 | target (url test-host "target_test_db")]
346 | (try
347 | (get-database source)
348 | (get-database target)
349 | (bulk-update source test-docs)
350 | (replicate-database source target)
351 | (is (= 4 (:total_rows (meta (all-documents target)))))
352 | (finally
353 | (delete-database source)
354 | (delete-database target)))))
355 |
356 | (defn report-change
357 | [description & forms]
358 | (doseq [result forms]
359 | (println (str "Testing changes: '" description "'") (if result "passed" "failed"))))
360 |
361 | (defn check-id-changes-test
362 | [description change-meta]
363 | (if-not (:last_seq change-meta)
364 | (report-change description
365 | (is (= (:id change-meta) "target-id")))))
366 |
367 | (defn check-seq-changes-test
368 | [description change-meta]
369 | (if-not (:last_seq change-meta)
370 | (report-change description
371 | (is (= (:seq change-meta) 1)))))
372 |
373 | (defn check-delete-changes-test
374 | [description change-meta]
375 | (if (:deleted change-meta)
376 | (report-change description
377 | (is (= (:id change-meta) "target-id"))
378 | (is (= (:seq change-meta) 5)))))
379 |
380 | (deftest direct-db-config-usage
381 | (let [db (test-database-url "direct-db-config-usage")]
382 | (try
383 | (create-database db)
384 | (let [doc (put-document db (first test-docs) :id "foo")]
385 | (update-document db doc {:a 5})
386 | (is (= (assoc (first test-docs) :a 5) (dissoc-meta (get-document db "foo")))))
387 | (finally
388 | (delete-database db)))))
389 |
390 | (deftest multiple-binding-levels
391 | (let [db1 (test-database-url "multiple-binding-levels")
392 | db2 (test-database-url "multiple-binding-levels2")]
393 | (with-db db1
394 | (try
395 | (is (= "multiple-binding-levels" (:db_name (get-database))))
396 | (put-document {} :id "1")
397 | (with-db db2
398 | (try
399 | (is (= "multiple-binding-levels2" (:db_name (get-database))))
400 | (is (nil? (get-document "1")))
401 | (let [doc (put-document {} :id "2")]
402 | (update-document doc {:a 5})
403 | (is (= {:a 5} (dissoc-meta (get-document "2")))))
404 | (finally
405 | (delete-database))))
406 | (is (nil? (get-document "2")))
407 | (finally
408 | (delete-database))))))
--------------------------------------------------------------------------------