Clutch is a Clojure library for Apache CouchDB.
To include Clutch in your project, simply add the following to your project.clj
dependencies:
[com.ashafa/clutch "0.3.0"]
and run…
lein deps
Or, if you’re using Maven, add this dependency to your pom.xml
:
<dependency> <groupId>com.ashafa</groupId> <artifactId>clutch</artifactId> <version>0.3.0-SNAPSHOT</version> </dependency>
Clutch is compatible with Clojure 1.2.0 and 1.3.0.
Although it’s in an early stage of development (Clutch API subject to change), Clutch supports most of the Apache CouchDB API:
-
Essentially all of the core document API
-
Most of the Database API, including
_changes
(implemented using watches) -
Views, including access, update, and a Clojure view server implementation
At the moment, you’ll have to look at the source or introspect the docs once you’ve loaded Clutch up to get around the API. Proper API documentation (via autodoc or marginalia) coming soon.
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.
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.
Clutch’s test suite is run on every commit thanks to Travis.
=> (get-database "clutch_example") ;; creates database if it's not available yet #com.ashafa.clutch.utils.URL{:protocol "http", :username nil, :password nil, :host "localhost", :port -1, :path "clutch_example", :query nil, :disk_format_version 5, :db_name "clutch_example", :doc_del_count 0, :committed_update_seq 0, :disk_size 79, :update_seq 0, :purge_seq 0, :compact_running false, :instance_start_time "1323701753566374", :doc_count 0} => (bulk-update "clutch_example" [{:test-grade 10 :_id "foo"} {:test-grade 20} {:test-grade 30}]) [{:id "foo", :rev "1-8a15da0db077cd05b45ec93b3a207d09"} {:id "0896fbf57128d7f1a1b238a52b0ec372", :rev "1-796ebf042b42fa3585332c3aa4a6f706"} {:id "0896fbf57128d7f1a1b238a52b0ecda8", :rev "1-01f063c5aeb1b63992c90c72c7a515ed"}] => (get-document "clutch_example" "foo") {:_id "foo", :_rev "1-8a15da0db077cd05b45ec93b3a207d09", :test-grade 10}
All Clutch functions accept a first argument indicating the database API endpoint for that operation. This argument can be a string URL, or, if the string is provided without a protocol, etc., it is assumed to be the name of a database on localhost:5984 as above:
=> (put-document "https://username:password`XXX.cloudant.com/databasename/" {:a 5 :b 6}) {:_id "36b807aacf227f921aa256b06ab094e5", :_rev "1-d4d04a5b59bcd73893a84de2d9595c4c", :a 5, :b 6}
Alternatively, Clutch defines its own record type for URLs:
=> (com.ashafa.clutch.utils/url "databasename") #com.ashafa.clutch.utils.URL{:protocol "http", :username nil, :password nil, :host "localhost", :port -1, :path "databasename", :query nil}
You can assoc
in whatever you like to a URL
record, which is handy for keeping database URLs and
credentials separate:
=> (def db (assoc (utils/url "https://XXX.cloudant.com/") :username "username" :password "password" :path "databasename")) #'test-clutch/db => (put-document db {:a 5 :b [0 6]}) {:_id "17e55bcc31e33dd30c3313cc2e6e5bb4", :_rev "1-a3517724e42612f9fbd350091a96593c", :a 5, :b [0 6]}
You can optionally provide configuration using dynamic scope via with-db
:
=> (with-db "clutch_example" (put-document {:_id "a" :a 5}) (put-document {:_id "b" :b 6}) (-> (get-document "a") (merge (get-document "b")) (dissoc-meta))) {:b 6, :a 5}
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:
java -cp <path to your uberjar> clojure.main -m com.ashafa.clutch.view-server
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:
user=> (use '[com.ashafa.clutch.view-server :only (view-server-exec-string)]) nil user=> (println (view-server-exec-string)) 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
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:
[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 ["log" "echo, please"] ["log",["echo, please"]]
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.
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:
(configure-view-server (view-server-exec-string))
Alternatively, use Futon to add the clojure
query server language to your CouchDB instance’s config.
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):
[query_servers] clojure = java -cp …rest of your exec string…
=> (configure-view-server "clutch_example" (com.ashafa.clutch.view-server/view-server-exec-string)) "" => (save-view "clutch_example" "demo_views" (view-server-fns :clojure {:sum {:map (fn [doc] [[nil (:test-grade doc)]]) :reduce (fn [keys values _] (apply + values))}})) {:_rev "1-ddc80a2c95e06b62dd2923663dc855aa", :views {:sum {:map "(fn [doc] [[nil (:test-grade doc)]])", :reduce "(fn [keys values _] (apply + values))"}}, :language :clojure, :_id "_design/demo_views"} => (-> (get-view "clutch_example" "demo_views" :sum) first :value) 60 => (get-view "clutch_example" "demo_views" :sum {:reduce false}) ({:id "0896fbf57128d7f1a1b238a52b0ec372", :key nil, :value 20} {:id "0896fbf57128d7f1a1b238a52b0ecda8", :key nil, :value 30} {:id "foo", :key nil, :value 10}) => (map :value (get-view "clutch_example" "demo_views" :sum {:reduce false})) (20 30 10)
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.
=> (meta (all-documents "databasename")) {:total_rows 20000, :offset 0}
Many breaking changes to refine/simplify the API, clean up the implementation, and add additional features:
Core API:
-
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. -
Renamed
update-attachment
⇒put-attachment
;filename
andmime-type
arguments now kwargs,InputStream
can now be provided as attachment data -
update-document
semantics have been simplified for the case where an "update function" and arguments are supplied to work well with core Clojure functions likeupdate-in
andassoc
(fixes issue #8) — e.g. can be used likeswap!
et al. -
Optional
:id
and:attachment
arguments toput-document
(wascreate-document
) are now specified via keyword arguments -
Removed "update map" argument from
bulk-update
fn (replace with e.g.(bulk-update db (map #(merge % update-map) documents)
) -
Renamed
get-all-documents-meta
⇒all-documents
-
com.ashafa.clutch.http-client/response-code
is no longer assumed to be an atom. Rather, it isset!
-ed directly when it is thread-bound. (Fixes issue #29)
View-related API:
-
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. -
elimination of inconsistency between APIs between
save-view
andsave-filter
. The names of individual views and filters are now part of the map provided to these functions, instead of sometimes being provided separately. -
:language
has been eliminated as part of the dynamically-bound configuration map -
with-clj-view-server
has been replaced by the more genericview-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. -
A
view-transformer
multimethod is now available, which opens up clutch to dynamically support additional view server languages. -
Moved
view-server-exec-string
tocom.ashafa.clutch.view-server
namespace
Appreciations go out to: