Skip to content

Commit b478441

Browse files
committed
Merge branch 'pr/215'
2 parents 5bc7248 + f300229 commit b478441

File tree

5 files changed

+180
-9
lines changed

5 files changed

+180
-9
lines changed

README.org

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,16 @@ More example requests:
149149
:content-type :json
150150
:json-opts {:date-format "yyyy-MM-dd"})
151151

152+
;; Send form params as a Transit encoded JSON body (POST or PUT) with options
153+
(client/post "http://site.com" {:form-params {:foo "bar"}
154+
:content-type :transit+json
155+
:transit-opts {:handlers {}}})
156+
157+
;; Send form params as a Transit encoded MessagePack body (POST or PUT) with options
158+
(client/post "http://site.com" {:form-params {:foo "bar"}
159+
:content-type :transit+msgpack
160+
:transit-opts {:handlers {}}})
161+
152162
;; Multipart form uploads/posts
153163
;; takes a vector of maps, to preserve the order of entities, :name
154164
;; will be used as the part name unless :part-name is specified
@@ -247,6 +257,10 @@ content encodings.
247257
(client/get "http://site.com/foo.json" {:as :json-string-keys})
248258
(client/get "http://site.com/foo.json" {:as :json-strict-string-keys})
249259

260+
;; Coerce as Transit encoded JSON or MessagePack
261+
(client/get "http://site.com/foo" {:as :transit+json})
262+
(client/get "http://site.com/foo" {:as :transit+msgpack})
263+
250264
;; Coerce as a clojure datastructure
251265
(client/get "http://site.com/foo.clj" {:as :clojure})
252266

project.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
[org.apache.httpcomponents/httpmime "4.3.5"]
1313
[commons-codec "1.9"]
1414
[commons-io "2.4"]
15+
[com.cognitect/transit-clj "0.8.247"]
1516
[slingshot "0.10.3"]
1617
[cheshire "5.3.1"]
1718
[crouton "0.1.2"]

src/clj_http/client.clj

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@
3737
true
3838
(catch Throwable _ false)))
3939

40+
;; Transit is an optional dependency, so check at compile time.
41+
(def transit-enabled?
42+
(try
43+
(require 'cognitect.transit)
44+
true
45+
(catch Throwable _ false)))
46+
4047
(defn ^:dynamic parse-edn
4148
"Resolve and apply tool.reader's EDN parsing."
4249
[& args]
@@ -51,6 +58,14 @@
5158
{:pre [crouton-enabled?]}
5259
(apply (ns-resolve (symbol "crouton.html") (symbol "parse")) args))
5360

61+
(defn ^:dynamic parse-transit
62+
"Resolve and apply Transit's JSON/MessagePack parsing."
63+
[& args]
64+
{:pre [transit-enabled?]}
65+
(let [reader (ns-resolve 'cognitect.transit 'reader)
66+
read (ns-resolve 'cognitect.transit 'read)]
67+
(read (apply reader args))))
68+
5469
(defn ^:dynamic json-encode
5570
"Resolve and apply cheshire's json encoding dynamically."
5671
[& args]
@@ -92,7 +107,7 @@
92107

93108
(defn url-encode-illegal-characters
94109
"Takes a raw url path or query and url-encodes any illegal characters.
95-
Minimizes ambiguity by encoding space to %20."
110+
Minimizes ambiguity by encoding space to %20."
96111
[path-or-query]
97112
(when path-or-query
98113
(-> path-or-query
@@ -311,6 +326,12 @@
311326
(binding [*read-eval* false]
312327
(assoc resp :body (read-string (String. ^"[B" body charset)))))))
313328

329+
(defn coerce-transit-body
330+
[{:keys [transit-opts] :as request} {:keys [body] :as resp} type]
331+
(if transit-enabled?
332+
(assoc resp :body (parse-transit body type transit-opts))
333+
resp))
334+
314335
(defmulti coerce-content-type (fn [req resp] (:content-type resp)))
315336

316337
(defmethod coerce-content-type :application/clojure [req resp]
@@ -322,6 +343,12 @@
322343
(defmethod coerce-content-type :application/json [req resp]
323344
(coerce-json-body req resp true false))
324345

346+
(defmethod coerce-content-type :application/transit+json [req resp]
347+
(coerce-transit-body req resp :json))
348+
349+
(defmethod coerce-content-type :application/transit+msgpack [req resp]
350+
(coerce-transit-body req resp :msgpack))
351+
325352
(defmethod coerce-content-type :default [req resp]
326353
(if-let [charset (-> resp :content-type-params :charset)]
327354
(coerce-response-body {:as charset} resp)
@@ -347,6 +374,12 @@
347374
(defmethod coerce-response-body :clojure [req resp]
348375
(coerce-clojure-body req resp))
349376

377+
(defmethod coerce-response-body :transit+json [req resp]
378+
(coerce-transit-body req resp :json))
379+
380+
(defmethod coerce-response-body :transit+msgpack [req resp]
381+
(coerce-transit-body req resp :msgpack))
382+
350383
(defmethod coerce-response-body :default
351384
[{:keys [as]} {:keys [status body] :as resp}]
352385
(let [body-bytes (util/force-byte-array body)]
@@ -590,6 +623,49 @@
590623
(assoc :request-method m)))
591624
(client req))))
592625

626+
(defmulti coerce-form-params
627+
(fn [req] (keyword (content-type-value (:content-type req)))))
628+
629+
(defmethod coerce-form-params :application/edn
630+
[{:keys [form-params]}]
631+
(pr-str form-params))
632+
633+
(defn- coerce-transit-form-params [type {:keys [form-params transit-opts]}]
634+
(when-not transit-enabled?
635+
(throw (ex-info (format (str "Can't encode form params as \"application/transit+%s\". "
636+
"Transit dependency not loaded.")
637+
(name type))
638+
{:type :transit-not-loaded
639+
:form-params form-params
640+
:transit-opts transit-opts
641+
:transit-type type})))
642+
(let [output (java.io.ByteArrayOutputStream.)
643+
writer (ns-resolve 'cognitect.transit 'writer)
644+
write (ns-resolve 'cognitect.transit 'write)
645+
_ (write (writer output type transit-opts) form-params)
646+
bytes (.toByteArray output)]
647+
(.reset output)
648+
bytes))
649+
650+
(defmethod coerce-form-params :application/transit+json [req]
651+
(coerce-transit-form-params :json req))
652+
653+
(defmethod coerce-form-params :application/transit+msgpack [req]
654+
(coerce-transit-form-params :msgpack req))
655+
656+
(defmethod coerce-form-params :application/json
657+
[{:keys [form-params json-opts]}]
658+
(when-not json-enabled?
659+
(throw (ex-info (str "Can't encode form params as \"application/json\". "
660+
"Cheshire dependency not loaded.")
661+
{:type :cheshire-not-loaded
662+
:form-params form-params
663+
:json-opts json-opts})))
664+
(json-encode form-params json-opts))
665+
666+
(defmethod coerce-form-params :default [{:keys [content-type form-params]}]
667+
(generate-query-string form-params (content-type-value content-type)))
668+
593669
(defn wrap-form-params
594670
"Middleware wrapping the submission or form parameters."
595671
[client]
@@ -600,10 +676,7 @@
600676
(client (-> req
601677
(dissoc :form-params)
602678
(assoc :content-type (content-type-value content-type)
603-
:body (if (and (= content-type :json) json-enabled?)
604-
(json-encode form-params json-opts)
605-
(generate-query-string form-params
606-
(content-type-value content-type))))))
679+
:body (coerce-form-params req))))
607680
(client req))))
608681

609682
(defn- nest-params

test/clj_http/test/client.clj

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,13 @@
280280
(deftest apply-on-accept
281281
(is-applied client/wrap-accept
282282
{:accept :json}
283-
{:headers {"accept" "application/json"}}))
283+
{:headers {"accept" "application/json"}})
284+
(is-applied client/wrap-accept
285+
{:accept :transit+json}
286+
{:headers {"accept" "application/transit+json"}})
287+
(is-applied client/wrap-accept
288+
{:accept :transit+msgpack}
289+
{:headers {"accept" "application/transit+msgpack"}}))
284290

285291
(deftest pass-on-no-accept
286292
(is-passed client/wrap-accept
@@ -349,7 +355,15 @@
349355
(is-applied client/wrap-content-type
350356
{:content-type :json :character-encoding "UTF-8"}
351357
{:headers {"content-type" "application/json; charset=UTF-8"}
352-
:content-type :json :character-encoding "UTF-8"}))
358+
:content-type :json :character-encoding "UTF-8"})
359+
(is-applied client/wrap-content-type
360+
{:content-type :transit+json}
361+
{:headers {"content-type" "application/transit+json"}
362+
:content-type :transit+json})
363+
(is-applied client/wrap-content-type
364+
{:content-type :transit+msgpack}
365+
{:headers {"content-type" "application/transit+msgpack"}
366+
:content-type :transit+msgpack}))
353367

354368
(deftest pass-on-no-content-type
355369
(is-passed client/wrap-content-type
@@ -437,6 +451,7 @@
437451
(is (= "param1=value1&param2=value2" (:body resp)))
438452
(is (= "application/x-www-form-urlencoded" (:content-type resp)))
439453
(is (not (contains? resp :form-params)))))
454+
440455
(testing "With json form params"
441456
(let [param-client (client/wrap-form-params identity)
442457
params {:param1 "value1" :param2 "value2"}
@@ -471,6 +486,40 @@
471486
(is (= (json/encode params {:date-format "yyyy-MM-dd"}) (:body resp)))
472487
(is (= "application/json" (:content-type resp)))
473488
(is (not (contains? resp :form-params)))))
489+
490+
(testing "With EDN form params"
491+
(doseq [method [:post :put :patch]]
492+
(let [param-client (client/wrap-form-params identity)
493+
params {:param1 "value1" :param2 "value2"}
494+
resp (param-client {:request-method method
495+
:content-type :edn
496+
:form-params params})]
497+
(is (= (pr-str params) (:body resp)))
498+
(is (= "application/edn" (:content-type resp)))
499+
(is (not (contains? resp :form-params))))))
500+
501+
(testing "With Transit/JSON form params"
502+
(doseq [method [:post :put :patch]]
503+
(let [param-client (client/wrap-form-params identity)
504+
params {:param1 "value1" :param2 "value2"}
505+
resp (param-client {:request-method method
506+
:content-type :transit+json
507+
:form-params params})]
508+
(is (= params (client/parse-transit (ByteArrayInputStream. (:body resp)) :json)))
509+
(is (= "application/transit+json" (:content-type resp)))
510+
(is (not (contains? resp :form-params))))))
511+
512+
(testing "With Transit/MessagePack form params"
513+
(doseq [method [:post :put :patch]]
514+
(let [param-client (client/wrap-form-params identity)
515+
params {:param1 "value1" :param2 "value2"}
516+
resp (param-client {:request-method method
517+
:content-type :transit+msgpack
518+
:form-params params})]
519+
(is (= params (client/parse-transit (ByteArrayInputStream. (:body resp)) :msgpack)))
520+
(is (= "application/transit+msgpack" (:content-type resp)))
521+
(is (not (contains? resp :form-params))))))
522+
474523
(testing "Ensure it does not affect GET requests"
475524
(let [param-client (client/wrap-form-params identity)
476525
resp (param-client {:request-method :get
@@ -479,6 +528,7 @@
479528
:param2 "value2"}})]
480529
(is (= "untouched" (:body resp)))
481530
(is (not (contains? resp :content-type)))))
531+
482532
(testing "with no form params"
483533
(let [param-client (client/wrap-form-params identity)
484534
resp (param-client {:body "untouched"})]
@@ -616,16 +666,26 @@
616666
(let [json-body (ByteArrayInputStream. (.getBytes "{\"foo\":\"bar\"}"))
617667
auto-body (ByteArrayInputStream. (.getBytes "{\"foo\":\"bar\"}"))
618668
edn-body (ByteArrayInputStream. (.getBytes "{:foo \"bar\"}"))
669+
transit-json-body (ByteArrayInputStream. (.getBytes "[\"^ \",\"~:foo\",\"bar\"]"))
670+
transit-msgpack-body (->> (map byte [-127 -91 126 58 102 111 111 -93 98 97 114])
671+
(byte-array 11)
672+
(ByteArrayInputStream.))
619673
json-resp {:body json-body :status 200
620674
:headers {"content-type" "application/json"}}
621675
auto-resp {:body auto-body :status 200
622676
:headers {"content-type" "application/json"}}
623677
edn-resp {:body edn-body :status 200
624-
:headers {"content-type" "application/edn"}}]
678+
:headers {"content-type" "application/edn"}}
679+
transit-json-resp {:body transit-json-body :status 200
680+
:headers {"content-type" "application/transit-json"}}
681+
transit-msgpack-resp {:body transit-msgpack-body :status 200
682+
:headers {"content-type" "application/transit-msgpack"}}]
625683
(is (= {:foo "bar"}
626684
(:body (client/coerce-response-body {:as :json} json-resp))
627685
(:body (client/coerce-response-body {:as :clojure} edn-resp))
628-
(:body (client/coerce-response-body {:as :auto} auto-resp))))))
686+
(:body (client/coerce-response-body {:as :auto} auto-resp))
687+
(:body (client/coerce-response-body {:as :transit+json} transit-json-resp))
688+
(:body (client/coerce-response-body {:as :transit+msgpack} transit-msgpack-resp))))))
629689

630690
(deftest ^:integration t-with-middleware
631691
(run-server)

test/clj_http/test/core.clj

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@
5151
[:get "/redirect-to-get"]
5252
{:status 302
5353
:headers {"location" "http://localhost:18080/get"}}
54+
[:get "/transit-json"]
55+
{:status 200 :body "[\"^ \",\"~:eggplant\",[\"^ \",\"~:quux\",[\"~#set\",[1,3,2]]],\"~:baz\",\"~f7\",\"~:foo\",\"bar\"]"
56+
:headers {"content-type" "application/transit+json"}}
57+
[:get "/transit-msgpack"]
58+
{:status 200
59+
:body (->> [-125 -86 126 58 101 103 103 112 108 97 110 116 -127 -90 126 58 113 117
60+
117 120 -110 -91 126 35 115 101 116 -109 1 3 2 -91 126 58 98 97 122 -93
61+
126 102 55 -91 126 58 102 111 111 -93 98 97 114]
62+
(map byte)
63+
(byte-array)
64+
(ByteArrayInputStream.))
65+
:headers {"content-type" "application/transit+msgpack"}}
5466
[:head "/head"]
5567
{:status 200}
5668
[:get "/content-type"]
@@ -324,6 +336,17 @@
324336
(:body clj-resp)
325337
(:body edn-resp)))))
326338

339+
(deftest ^:integration t-transit-output-coercion
340+
(run-server)
341+
(let [transit-json-resp (client/get (localhost "/transit-json") {:as :auto})
342+
transit-msgpack-resp (client/get (localhost "/transit-msgpack") {:as :auto})]
343+
(is (= 200
344+
(:status transit-json-resp)
345+
(:status transit-msgpack-resp)))
346+
(is (= {:foo "bar" :baz 7M :eggplant {:quux #{1 2 3}}}
347+
(:body transit-json-resp)
348+
(:body transit-msgpack-resp)))))
349+
327350
(deftest ^:integration t-json-output-coercion
328351
(run-server)
329352
(let [resp (client/get (localhost "/json") {:as :json})

0 commit comments

Comments
 (0)