#lang racket
(require (planet dherman/json)
net/url
net/head
net/uri-codec
net/base64)
(define-struct/contract
couchdb-server ((host string?)
(port exact-nonnegative-integer?)
(user (or/c string? false?))
(password (or/c string? false?))))
(define-struct/contract
couchdb-database ((server couchdb-server?)
(name string?)))
(define-struct (exn:couchdb exn) (error url doc))
(define-struct (exn:couchdb:not-found exn:couchdb) ())
(define-struct (exn:couchdb:conflict exn:couchdb) ())
(define exn-map (hash "not_found" make-exn:couchdb:not-found
"conflict" make-exn:couchdb:conflict))
(provide
couchdb-server?
couchdb-database?
exn:couchdb?
exn:couchdb:conflict?
exn:couchdb:not-found?)
(provide (contract-out
(couchdb-connect (->* ()
(#:host string?
#:port exact-nonnegative-integer?
#:user (or/c string? #f)
#:password (or/c string? #f))
couchdb-server?))
(couchdb-db (-> couchdb-server?
string?
couchdb-database?))
(couchdb-info (-> (or/c couchdb-server? couchdb-database?)
jsexpr?))
(couchdb-all-dbs (-> couchdb-server?
jsexpr?))
(couchdb-all-docs (-> couchdb-database?
jsexpr?))
(couchdb-uuids (->* (couchdb-server?)
(#:count exact-nonnegative-integer?)
(listof string?)))
(couchdb-get (->* (couchdb-database?
string?)
(#:rev (or/c string?
(symbols 'current))
#:open-revs (or/c (symbols 'all 'current)
(listof string?))
#:revs-info? boolean?
#:conflicts? boolean?)
jsexpr?))
(couchdb-put (-> couchdb-database?
jsexpr?
jsexpr?))
(couchdb-update (-> couchdb-database?
string?
(-> jsexpr? jsexpr?)
jsexpr?))
(couchdb-update/document (-> couchdb-database?
jsexpr?
(-> jsexpr? jsexpr?)
jsexpr?))
(couchdb-delete (-> couchdb-database?
jsexpr?
jsexpr?))
(couchdb-delete-db (-> couchdb-database?
jsexpr?))
(couchdb-view (->* (couchdb-database?
(list/c string? string?))
(#:include-docs? boolean?
#:key (or/c jsexpr? void?)
#:startkey (or/c jsexpr? void?)
#:startkey-docid (or/c jsexpr? void?)
#:endkey (or/c jsexpr? void?)
#:endkey-docid (or/c jsexpr? void?)
#:limit (or/c exact-nonnegative-integer? void?)
#:stale (or/c (symbols 'ok 'update-after) void?)
#:descending? boolean?
#:skip exact-nonnegative-integer?
#:group? boolean?
#:group-level (or/c exact-nonnegative-integer?
void?)
#:reduce? (or/c boolean? void?)
#:inclusive-end? boolean?
#:update-seq? boolean?)
jsexpr?))))
(define (couchdb-connect #:host (host "localhost")
#:port (port 5984)
#:user (user #f)
#:password (password #f))
(make-couchdb-server host port user password))
(define (couchdb-db server name)
(make-couchdb-database server name))
(define (make-server-url server path (query null))
(make-url "http"
(couchdb-server-user server)
(couchdb-server-host server)
(couchdb-server-port server)
#t
(map (lambda (c) (make-path/param c null)) path)
query
#f))
(define (make-database-url db path (query null))
(make-server-url (couchdb-database-server db)
(cons (couchdb-database-name db) path)
query))
(define (make-view-url db view (query null))
(let ((design (car view))
(view (cadr view)))
(make-database-url db (list "_design" design "_view" view) query)))
(define (encode-open-revs open-revs)
(if (symbol? open-revs)
(symbol->string open-revs)
(jsexpr->json open-revs)))
(define (raise-error ccm doc method url)
(let* ((error (hash-ref doc 'error))
(reason (string-append "CouchDB " method " " (url->string url)
"\n " error
": " (hash-ref doc 'reason))))
(if (hash-has-key? exn-map error)
(raise ((hash-ref exn-map error) reason ccm error url doc))
(raise (make-exn:couchdb reason ccm error url doc)))))
(define (error-document? doc)
(and (hash? doc)
(= (hash-count doc) 2)
(hash-has-key? doc 'error)
(hash-has-key? doc 'reason)))
(define (get-url url (header null))
(let* ((port (get-pure-port url header #:redirections 1))
(doc (read-json port)))
(if (error-document? doc)
(raise-error (current-continuation-marks) doc "GET" url)
doc)))
(define (put-url url data (header null))
(let* ((port (put-pure-port url data header))
(doc (read-json port)))
(if (error-document? doc)
(raise-error (current-continuation-marks) doc "PUT" url)
doc)))
(define (delete-url url (header null))
(let* ((port (delete-pure-port url header))
(doc (read-json port)))
(if (error-document? doc)
(raise-error (current-continuation-marks) doc "DELETE" url)
doc)))
(define (base64-character? char)
(or (and (char>=? char #\A)
(char<=? char #\Z))
(and (char>=? char #\a)
(char<=? char #\z))
(and (char>=? char #\0)
(char<=? char #\9))
(char=? char #\=)
(char=? char #\/)))
(define (base64-encode/string str)
(list->string
(filter base64-character?
(string->list
(bytes->string/utf-8
(base64-encode
(string->bytes/utf-8 str)))))))
(define (auth-header server-or-db)
(define-values (login password)
(if (couchdb-server? server-or-db)
(values (couchdb-server-user server-or-db)
(couchdb-server-password server-or-db))
(let ((server (couchdb-database-server server-or-db)))
(values (couchdb-server-user server)
(couchdb-server-password server)))))
(if (and login password)
(list (string-append "Authorization: Basic "
(base64-encode/string
(string-append login ":" password))))
null))
(define (couchdb-info server-or-db)
(if (couchdb-server? server-or-db)
(let ((url (make-server-url server-or-db null)))
(get-url url (auth-header server-or-db)))
(let ((url (make-database-url server-or-db null)))
(get-url url (auth-header server-or-db)))))
(define (couchdb-delete-db db)
(let* ((url (make-database-url db null)))
(delete-url url (auth-header db))))
(define (couchdb-all-dbs server)
(let* ((url (make-server-url server (list "_all_dbs"))))
(get-url url (auth-header server))))
(define (couchdb-all-docs db)
(let* ((url (make-database-url db (list "_all_docs"))))
(get-url url (auth-header db))))
(define (couchdb-uuids server #:count (count 1))
(let* ((url (make-server-url server (list "_uuids")
`((count . ,(number->string count))))))
(hash-ref (get-url url (auth-header server)) 'uuids)))
(define (couchdb-get db id
#:rev (rev 'current)
#:open-revs (open-revs 'current)
#:revs-info? (revs-info? #f)
#:conflicts? (conflicts? #f))
(define params null)
(define (add-param name value)
(set! params (append params (list (cons name value)))))
(or (equal? rev 'current)
(add-param 'rev rev))
(or (equal? open-revs 'current)
(add-param 'open_revs (encode-open-revs open-revs)))
(and revs-info?
(add-param 'revs_info "true"))
(and conflicts?
(add-param 'conflicts "true"))
(let* ((url (make-database-url db (list id) params)))
(get-url url (auth-header db))))
(define (couchdb-put db document)
(let* ((id (hash-ref document '_id))
(url (make-database-url db (list id))))
(put-url url
(string->bytes/utf-8 (jsexpr->json document))
(auth-header db))))
(define (couchdb-update/document db document update-fn)
(let loop ((current document))
(with-handlers* ((exn:couchdb:conflict?
(lambda (exn)
(loop (couchdb-get db (hash-ref current '_id))))))
(couchdb-put db (update-fn current)))))
(define (couchdb-update db id update-fn)
(couchdb-update/document db (couchdb-get db id) update-fn))
(define (couchdb-delete db document)
(let* ((id (hash-ref document '_id))
(rev (hash-ref document '_rev))
(url (make-database-url db (list id) `((rev . ,rev)))))
(delete-url url (auth-header db))))
(define (couchdb-view db view #:include-docs? (include-docs? #f)
#:key (key (void))
#:startkey (startkey (void))
#:startkey-docid (startkey-docid (void))
#:endkey (endkey (void))
#:endkey-docid (endkey-docid (void))
#:limit (limit (void))
#:stale (stale (void))
#:descending? (descending? #f)
#:skip (skip 0)
#:group? (group? #f)
#:group-level (group-level (void))
#:reduce? (reduce? (void))
#:inclusive-end? (inclusive-end? #t)
#:update-seq? (update-seq? #f))
(define params null)
(define (add-param name value)
(set! params (cons (cons name value) params)))
(and include-docs?
(add-param 'include_docs "true"))
(or (void? key)
(add-param 'key (jsexpr->json key)))
(or (void? startkey)
(add-param 'startkey (jsexpr->json startkey)))
(or (void? startkey-docid)
(add-param 'startkey_docid (jsexpr->json startkey-docid)))
(or (void? endkey)
(add-param 'endkey (jsexpr->json endkey)))
(or (void? endkey-docid)
(add-param 'endkey_docid (jsexpr->json endkey-docid)))
(or (void? limit)
(add-param 'limit (number->string limit)))
(or (void? stale)
(add-param 'stale (if (equal? stale 'ok) "ok" "update_after")))
(and descending?
(add-param 'descending "true"))
(and (> skip 0)
(add-param 'skip (number->string skip)))
(and group?
(add-param 'group "true"))
(or (void? group-level)
(add-param 'group_level (number->string group-level)))
(or (void? reduce?)
(add-param 'reduce (if reduce? "true" "false")))
(or inclusive-end?
(add-param 'inclusive_end "false"))
(and update-seq?
(add-param 'update_seq "true"))
(let* ((url (make-view-url db view params)))
(get-url url (auth-header db))))