├── .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 [![Travis CI status](https://secure.travis-ci.org/clojure-clutch/clutch.png)](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)))))) --------------------------------------------------------------------------------