Skip to content

Commit

Permalink
Merge pull request #23 from ngrunwald/conditional
Browse files Browse the repository at this point in the history
Adds support for conditional requests and new way of checking rate limits
  • Loading branch information
Raynes committed Apr 16, 2013
2 parents a9b8ee4 + 01910a2 commit 64dc5f2
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 25 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ If an API function has no options and authentication would have no uses for that

Authentication is supported by Github user authentication `:auth <username:password>` as demonstrated above, or by oauth or oauth2. For oauth add `:oauth-token <token>` to the options map. Likewise, for oauth2, include `:client-id <client_id> :client-token <client_token>` in the options map.

You can access useful information returned by the API such as current
rate limits, etags, etc. by checking the response with `core/api-meta`. You can then use this to perform conditional requests against the API. If the data has not changed, the keyword `:tentacles.core/not-modified` will be returned. This does not consume any API call quota.

```clojure
user> (core/api-meta (repos/readme "Raynes" "tentacles" {}))
{:links {nil nil}, :etag "\"f1f3cfabbf0f98e0bbaa7aa424f92e75\"", :last-modified "Mon, 28 Jan 2013 21:13:48 GMT", :call-limit 60, :call-remaining 59}

user> (repos/readme "Raynes" "tentacles" {:etag "\"f1f3cfabbf0f98e0bbaa7aa424f92e75\""})
:tentacles.core/not-modified

user> (repos/readme "Raynes" "tentacles" {:if-modified-since "Mon, 28 Jan 2013 21:13:48 GMT"})
:tentacles.core/not-modified
```

The Github API is massive and great. I can't demonstrate every API call. Everything is generally just as easy as the above examples, and I'm working hard to document things as well as possible, so go explore!

Here are some lovely [Marginalia docs](http://raynes.github.com/tentacles). I also wrote a demonstrational [blog post](http://blog.raynes.me/blog/2011/12/02/waving-our-tentacles/) about Tentacles that I intend to keep updated with future releases.
Expand Down
62 changes: 42 additions & 20 deletions src/tentacles/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,37 @@
(map parse-link)
(into {})))

(defn merge-rate-limit [m h]
"Merges RateLimit values from headers into Json response"
(merge m (select-keys h [:X-RateLimit-Limit :X-RateLimit-Remaining])))
(defn extract-useful-meta
[h]
(let [{:strs [etag last-modified x-ratelimit-limit x-ratelimit-remaining]} h]
{:etag etag :last-modified last-modified
:call-limit (when x-ratelimit-limit (Long/parseLong x-ratelimit-limit))
:call-remaining (when x-ratelimit-remaining (Long/parseLong x-ratelimit-remaining))}))

(defn api-meta
[obj]
(:api-meta (meta obj)))

(defn safe-parse
"Takes a response and checks for certain status codes. If 204, return nil.
If 400, 401, 204, 422, 403, 404 or 500, return the original response with the body parsed
as json. Otherwise, parse and return the body if json, or return the body if raw."
[resp]
(if (#{400 401 204 422 403 404 500} (:status resp))
(update-in resp [:body] parse-json)
(let [links (parse-links (get-in resp [:headers "link"] ""))
content-type (get-in resp [:headers "content-type"])]
(if-not (.contains content-type "raw")
(with-meta (merge-rate-limit (parse-json (:body resp)) (:headers resp)) {:links links})
(resp :body)))))
[{:keys [headers status body] :as resp}]
(cond
(= 304 status)
::not-modified
(#{400 401 204 422 403 404 500} status)
(update-in resp [:body] parse-json)
:else (let [links (parse-links (get headers "link" ""))
content-type (get headers "content-type")
metadata (extract-useful-meta headers)]
(if-not (.contains content-type "raw")
(let [parsed (parse-json body)]
(if (map? parsed)
(with-meta parsed {:links links :api-meta metadata})
(with-meta (map #(with-meta % metadata) parsed)
{:links links :api-meta metadata})))
body))))

(defn update-req
"Given a clj-http request, and a 'next' url string, merge the next url into the request"
Expand All @@ -64,18 +79,25 @@
[end-point positional]
(str url (apply format end-point (map #(URLEncoder/encode (str %) "UTF-8") positional))))

(defn make-request [method end-point positional query]
(defn make-request [method end-point positional
{:strs [auth throw_exceptions follow_redirects
accept oauth_token etag if_modified_since]
:or {follow_redirects true throw_exceptions false}
:as query}]
(let [req (merge-with merge
{:url (format-url end-point positional)
:basic-auth (query "auth")
:throw-exceptions (or (query "throw_exceptions") false)
:follow-redirects (let [over (get query "follow_redirects" ::not-found)]
(if (= over ::not-found) true over))
:basic-auth auth
:throw-exceptions throw_exceptions
:follow-redirects follow_redirects
:method method}
(when (query "accept")
{:headers {"Accept" (query "accept")}})
(when (query "oauth_token")
{:headers {"Authorization" (str "token " (query "oauth_token"))}}))
(when accept
{:headers {"Accept" accept}})
(when oauth_token
{:headers {"Authorization" (str "token " oauth_token)}})
(when etag
{:headers {"if-None-Match" etag}})
(when if_modified_since
{:headers {"if-Modified-Since" if_modified_since}}))
proper-query (dissoc query "auth" "oauth_token" "all_pages" "accept")
req (if (#{:post :put :delete} method)
(assoc req :body (json/generate-string (or (proper-query "raw") proper-query)))
Expand Down
9 changes: 4 additions & 5 deletions test/tentacles/core_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
(is (= (:status (core/safe-parse {:status 403}))
403)))

(deftest rate-limit-details-are-propagated-when-defined
(is (contains? (core/safe-parse {:status 200 :X-RateLimit-Limit 20 :headers {"content-type" ""}}) :X-RateLimit-Limit)))

(deftest rate-limit-details-are-ignored-when-undefined
(is (not (contains? (core/safe-parse {:status 200 :headers {"content-type" ""}}) :X-RateLimit-Limit))))
(deftest rate-limit-details-are-propagated
(is (= 60 (:call-limit (core/api-meta
(core/safe-parse {:status 200 :headers {"x-ratelimit-limit" "60"
"content-type" ""}}))))))

0 comments on commit 64dc5f2

Please sign in to comment.