Skip to content

Commit 50314a9

Browse files
authored
[#9] Always return map with :headers, :body and :status (#10)
1 parent 57daab0 commit 50314a9

File tree

3 files changed

+76
-80
lines changed

3 files changed

+76
-80
lines changed

README.md

+22-21
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,23 @@ Simple `GET` request:
1818

1919
``` clojure
2020
(curl/get "https://httpstat.us/200")
21-
;;=> "200 OK"
22-
```
23-
24-
Simple `GET` request returned as a map with `:body`, `:status` and `:headers`:
25-
26-
``` clojure
27-
(curl/get "https://httpstat.us/200" {:response true})
28-
;;=> {:status 200 :body "200 OK" :headers { ... }}
21+
;;=> {:status 200, :body "200 OK", :headers { ... }}
2922
```
3023

3124
Passing headers:
3225

3326
``` clojure
3427
(require '[cheshire.core :as json])
3528
(def resp (curl/get "https://httpstat.us/200" {:headers {"Accept" "application/json"}}))
36-
(json/parse-string resp) ;;=> {"code" 200, "description" "OK"}
29+
(json/parse-string (:body resp)) ;;=> {"code" 200, "description" "OK"}
3730
```
3831

3932
Query parameters:
4033

4134
``` clojure
4235
(->
4336
(curl/get "https://postman-echo.com/get" {:query-params {"q" "clojure"}})
37+
:body
4438
(json/parse-string true)
4539
:args)
4640
;;=> {:q "clojure"}
@@ -50,43 +44,48 @@ A `POST` request with a `:body`:
5044

5145
``` clojure
5246
(def resp (curl/post "https://postman-echo.com/post" {:body "From Clojure"}))
53-
(json/parse-string resp) ;;=> {"args" {}, "data" "", ...}
47+
(json/parse-string (:body resp)) ;;=> {"args" {}, "data" "", ...}
5448
```
5549

5650
Posting a file as a `POST` body:
5751

5852
``` clojure
59-
(curl/post "https://postman-echo.com/post" {:body (io/file "README.md")})
53+
(require '[clojure.java.io :as io])
54+
(:status (curl/post "https://postman-echo.com/post" {:body (io/file "README.md")}))
55+
;; => 100
6056
```
6157

6258
Posting form params:
6359

6460
``` clojure
65-
(curl/post "https://postman-echo.com/post" {:form-params {"name" "Michiel"}})
61+
(:status (curl/post "https://postman-echo.com/post" {:form-params {"name" "Michiel"}}))
62+
;; => 100
6663
```
6764

6865
Basic auth:
6966

7067
``` clojure
71-
(curl/get "https://postman-echo.com/basic-auth" {:basic-auth ["postman" "password"]})
68+
(:body (curl/get "https://postman-echo.com/basic-auth" {:basic-auth ["postman" "password"]}))
69+
;; => "{\"authenticated\":true}"
7270
```
7371

7472
Download a binary file as a stream:
7573

7674
``` clojure
77-
(require '[clojure.java.io :as io])
7875
(io/copy
79-
(curl/get "https://github.com/borkdude/babashka/raw/master/logo/icon.png"
80-
{:as :stream})
76+
(:body (curl/get "https://github.com/borkdude/babashka/raw/master/logo/icon.png"
77+
{:as :stream}))
8178
(io/file "icon.png"))
79+
(.length (io/file "icon.png"))
80+
;;=> 7748
8281
```
8382

8483
Passing raw arguments to `curl` can be done with `:raw-args`:
8584

8685
``` clojure
87-
(require '[clojure.string :as str])
88-
(def resp (curl/get "https://www.clojure.org" {:raw-args ["-D" "-"]}))
89-
(-> (str/split resp #"\n") first) ;;=> "HTTP/1.1 200 OK\r"
86+
(:status (curl/get "http://www.clojure.org" {:raw-args ["--max-redirs" "0"]}))
87+
curl: (47) Maximum (0) redirects followed
88+
301
9089
```
9190

9291
Talking to a UNIX socket:
@@ -95,10 +94,11 @@ Talking to a UNIX socket:
9594
(-> (curl/get "http://localhost/images/json"
9695
{:raw-args ["--unix-socket"
9796
"/var/run/docker.sock"]})
97+
:body
9898
(json/parse-string true)
9999
first
100100
:RepoTags)
101-
;;=> ["borkdude/babashka:0.0.73-SNAPSHOT"]
101+
;;=> ["borkdude/babashka:0.0.79-SNAPSHOT"]
102102
```
103103

104104
Using the low-level API for fine grained(and safer) URL construction:
@@ -109,6 +109,7 @@ Using the low-level API for fine grained(and safer) URL construction:
109109
:port 443
110110
:path "/get"
111111
:query "q=test"}})
112+
:body
112113
(json/parse-string true))
113114
;;=>
114115
{:args {:q "test"},
@@ -125,7 +126,7 @@ Using the low-level API for fine grained(and safer) URL construction:
125126
## Test
126127

127128
``` clojure
128-
$ clj -A:test
129+
$ clojure -A:test
129130
```
130131

131132
## License

src/babashka/curl.clj

+26-32
Original file line numberDiff line numberDiff line change
@@ -19,31 +19,18 @@
1919
return it.
2020
`:throw?`: Unless `false`, exits script when the shell-command has a
2121
non-zero exit code, unless `throw?` is set to false."
22-
([args] (shell-command args nil))
23-
([args {:keys [:throw? :as-stream?]
24-
:or {throw? true}}]
25-
(let [pb (let [pb (ProcessBuilder. ^java.util.List args)]
26-
(doto pb
27-
(.redirectInput ProcessBuilder$Redirect/INHERIT)
28-
(.redirectError ProcessBuilder$Redirect/INHERIT)))
29-
proc (.start pb)
30-
out
31-
(if as-stream? (.getInputStream proc)
32-
(let [sw (java.io.StringWriter.)]
33-
(with-open [w (io/reader (.getInputStream proc))]
34-
(io/copy w sw))
35-
(str sw)))
36-
exit-code (when-not as-stream? (.waitFor proc))]
37-
(when (and throw?
38-
(not as-stream?)
39-
(not (zero? exit-code)))
40-
(throw (ex-info "Got non-zero exit code" {:status exit-code})))
41-
{:out out
42-
:exit exit-code
43-
:err ""})))
22+
[args]
23+
(let [pb (let [pb (ProcessBuilder. ^java.util.List args)]
24+
(doto pb
25+
(.redirectInput ProcessBuilder$Redirect/INHERIT)
26+
(.redirectError ProcessBuilder$Redirect/INHERIT)))
27+
proc (.start pb)
28+
out (.getInputStream proc)]
29+
{:out out
30+
:proc proc}))
4431

4532
(defn- exec-curl [args opts]
46-
(let [res (shell-command args opts)
33+
(let [res (shell-command args)
4734
exit (:exit res)
4835
out (:out res)]
4936
;; TODO: handle non-zero exit with exception? TODO: should we return a map
@@ -124,7 +111,7 @@
124111
basic-auth)
125112
basic-auth (when basic-auth
126113
["--user" basic-auth])]
127-
(conj (reduce into ["curl" "--silent" "--show-error" "--location"]
114+
(conj (reduce into ["curl" "--silent" "--show-error" "--location" "--include"]
128115
[method headers accept-header data-raw in-file basic-auth
129116
form-params (:raw-args opts)])
130117
(str url
@@ -150,17 +137,26 @@
150137
(recur)))))
151138
(str sb)))
152139

140+
(defn- read-headers [^PushbackInputStream is]
141+
(loop [headers []]
142+
(let [next-line (read-line is)]
143+
(if (str/blank? next-line)
144+
headers
145+
(recur (conj headers next-line))))))
146+
153147
(defn- curl-response->map
154148
"Parses a curl response input stream into a map"
155149
[^java.io.InputStream input-stream opts]
156150
(let [input-stream (PushbackInputStream. input-stream)]
157151
(loop [redirects []]
158-
(let [[status-line & header-lines]
159-
(take-while #(not (str/blank? %)) (repeatedly #(read-line input-stream)))
152+
(let [headers (read-headers input-stream)
153+
[status-line & header-lines] headers
160154
status (Integer/parseInt (second (str/split status-line #" ")))
161155
headers (reduce (fn [acc header-line]
162156
(let [[k v] (str/split header-line #":" 2)]
163-
(assoc acc (str/lower-case k) (str/trim v))))
157+
(if (and k v)
158+
(assoc acc (str/lower-case k) (str/trim v))
159+
acc)))
164160
{}
165161
header-lines)
166162
response {:status status
@@ -181,13 +177,11 @@
181177
;;;; End Response Parsing
182178

183179
(defn request [opts]
184-
(let [args (curl-command opts)
185-
stream? (identical? :stream (:as opts))]
180+
(let [args (curl-command opts)]
186181
(when (:debug? opts)
187182
(println (str/join " " (map pr-str args))))
188-
(if (:response opts)
189-
(curl-response->map (exec-curl (conj args "--include") (assoc opts :as-stream? true)) opts)
190-
(exec-curl args (assoc opts :as-stream? stream?)))))
183+
(-> (exec-curl args opts)
184+
(curl-response->map opts))))
191185

192186
(defn head
193187
([url] (head url nil))

test/babashka/curl_test.clj

+28-27
Original file line numberDiff line numberDiff line change
@@ -6,85 +6,84 @@
66
[clojure.test :refer [deftest is testing are]]))
77

88
(deftest get-test
9-
(is (str/includes? (curl/get "https://httpstat.us/200")
9+
(is (str/includes? (:body (curl/get "https://httpstat.us/200"))
1010
"200"))
1111
(is (= 200
1212
(-> (curl/get "https://httpstat.us/200"
1313
{:headers {"Accept" "application/json"}})
14+
:body
1415
(json/parse-string true)
1516
:code)))
1617
(testing "query params"
1718
(is (= {:foo1 "bar1", :foo2 "bar2"}
1819
(-> (curl/get "https://postman-echo.com/get" {:query-params {"foo1" "bar1" "foo2" "bar2"}})
20+
:body
1921
(json/parse-string true)
2022
:args)))))
2123

2224
(deftest head-test
23-
(is (str/includes? (curl/head "https://postman-echo.com/head")
24-
"200 OK")))
25+
(is (= 200 (:status (curl/head "https://postman-echo.com/head")))))
2526

2627
(deftest post-test
27-
(is (subs (curl/post "https://postman-echo.com/post")
28+
(is (subs (:body (curl/post "https://postman-echo.com/post"))
2829
0 10))
2930
(is (str/includes?
30-
(curl/post "https://postman-echo.com/post"
31-
{:body "From Clojure"})
31+
(:body (curl/post "https://postman-echo.com/post"
32+
{:body "From Clojure"}))
3233
"From Clojure"))
3334
(testing "file-body"
3435
(is (str/includes?
35-
(curl/post "https://postman-echo.com/post"
36-
{:body (io/file "README.md")})
36+
(:body (curl/post "https://postman-echo.com/post"
37+
{:body (io/file "README.md")}))
3738
"babashka.curl")))
3839
(testing "form-params"
3940
(is (str/includes?
40-
(curl/post "https://postman-echo.com/post"
41-
{:form-params {"name" "michiel"}})
41+
(:body (curl/post "https://postman-echo.com/post"
42+
{:form-params {"name" "michiel"}}))
4243
"michiel"))))
4344

4445
(deftest patch-test
4546
(is (str/includes?
46-
(curl/patch "https://postman-echo.com/patch"
47-
{:body "hello"})
47+
(:body (curl/patch "https://postman-echo.com/patch"
48+
{:body "hello"}))
4849
"hello")))
4950

5051
(deftest basic-auth-test
5152
(is (re-find #"authenticated.*true"
52-
(curl/get "https://postman-echo.com/basic-auth" {:basic-auth ["postman" "password"]}))))
53+
(:body
54+
(curl/get "https://postman-echo.com/basic-auth"
55+
{:basic-auth ["postman" "password"]})))))
5356

5457
(deftest raw-args-test
55-
(is (str/includes?
56-
(curl/post "https://postman-echo.com/post"
57-
{:body "From Clojure"
58-
:raw-args ["-D" "-"]})
59-
"200 OK")))
58+
(is (= 200 (:status (curl/post "https://postman-echo.com/post"
59+
{:body "From Clojure"
60+
:raw-args ["-D" "-"]})))))
6061

6162
(deftest get-response-object-test
62-
(let [response (curl/get "https://httpstat.us/200" {:response true})]
63+
(let [response (curl/get "https://httpstat.us/200")]
6364
(is (map? response))
6465
(is (= 200 (:status response)))
6566
(is (= "200 OK" (:body response)))
6667
(is (= "Microsoft-IIS/10.0" (get-in response [:headers "server"]))))
6768

6869
(testing "response object as stream"
69-
(let [response (curl/get "https://httpstat.us/200" {:response true :as :stream})]
70+
(let [response (curl/get "https://httpstat.us/200" {:as :stream})]
7071
(is (map? response))
7172
(is (= 200 (:status response)))
7273
(is (instance? java.io.InputStream (:body response)))
7374
(is (= "200 OK" (slurp (:body response))))))
7475

7576
(testing "response object with following redirect"
7677
(let [response (curl/get "https://httpbin.org/redirect-to?url=https://www.httpbin.org"
77-
{:raw-args ["-L"]
78-
:response true})]
78+
{:raw-args ["-L"]})]
7979
(is (map? response))
8080
(is (= 200 (:status response)))
8181
(is (= 302 (-> response :redirects first :status)))
8282
(is (= "https://www.httpbin.org" (get-in response [:redirects 0 :headers "location"])))))
8383

8484
(testing "response object without fully following redirects"
8585
(let [response (curl/get "https://httpbin.org/redirect-to?url=https://www.httpbin.org"
86-
{:response true
87-
:raw-args ["--max-redirs" "0"]})]
86+
{:raw-args ["--max-redirs" "0"]})]
8887
(is (map? response))
8988
(is (= 302 (:status response)))
9089
(is (= "" (:body response)))
@@ -95,12 +94,14 @@
9594
(is (= 200
9695
(-> (curl/get "https://httpstat.us/200"
9796
{:accept :json})
97+
:body
9898
(json/parse-string true)
9999
:code))))
100100

101101
(deftest url-encode-query-params-test
102102
(is (= {"my query param?" "hello there"}
103103
(-> (curl/get "https://postman-echo.com/get" {:query-params {"my query param?" "hello there"}})
104+
:body
104105
(json/parse-string)
105106
(get "args")))))
106107

@@ -110,6 +111,7 @@
110111
:port 443
111112
:path "/get"
112113
:query "q=test"}})
114+
:body
113115
(json/parse-string true))]
114116
(is (= {:q "test"} (:args response)))
115117
(is (= "httpbin.org" (get-in response [:headers :Host])))))
@@ -118,15 +120,14 @@
118120
(testing "download image"
119121
(let [tmp-file (java.io.File/createTempFile "icon" ".png")]
120122
(.deleteOnExit tmp-file)
121-
(io/copy (curl/get "https://github.com/borkdude/babashka/raw/master/logo/icon.png" {:as :stream})
123+
(io/copy (:body (curl/get "https://github.com/borkdude/babashka/raw/master/logo/icon.png" {:as :stream}))
122124
tmp-file)
123125
(is (= (.length (io/file "test" "icon.png"))
124126
(.length tmp-file)))))
125127
(testing "download image with response headers"
126128
(let [tmp-file (java.io.File/createTempFile "icon" ".png")]
127129
(.deleteOnExit tmp-file)
128-
(let [resp (curl/get "https://github.com/borkdude/babashka/raw/master/logo/icon.png" {:as :stream
129-
:response true})]
130+
(let [resp (curl/get "https://github.com/borkdude/babashka/raw/master/logo/icon.png" {:as :stream})]
130131
(is (= 200 (:status resp)))
131132
(io/copy (:body resp) tmp-file))
132133
(is (= (.length (io/file "test" "icon.png"))

0 commit comments

Comments
 (0)