Skip to content

Commit

Permalink
Support clojure function in :client option (#54)
Browse files Browse the repository at this point in the history
  • Loading branch information
borkdude committed Apr 18, 2024
1 parent 9a71681 commit e7c4631
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 35 deletions.
31 changes: 16 additions & 15 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ Options used to create the (implicit) default client.
```

Convenience wrapper for [`request`](#babashka.http-client/request) with method `:delete`
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client.clj#L133-L138">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client.clj#L134-L139">Source</a></sub></p>

## <a name="babashka.http-client/get">`get`</a><a name="babashka.http-client/get"></a>
``` clojure
Expand All @@ -190,7 +190,7 @@ Convenience wrapper for [`request`](#babashka.http-client/request) with method `
```

Convenience wrapper for [`request`](#babashka.http-client/request) with method `:get`
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client.clj#L126-L131">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client.clj#L127-L132">Source</a></sub></p>

## <a name="babashka.http-client/head">`head`</a><a name="babashka.http-client/head"></a>
``` clojure
Expand All @@ -200,7 +200,7 @@ Convenience wrapper for [`request`](#babashka.http-client/request) with method `
```

Convenience wrapper for [`request`](#babashka.http-client/request) with method `:head`
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client.clj#L140-L145">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client.clj#L141-L146">Source</a></sub></p>

## <a name="babashka.http-client/patch">`patch`</a><a name="babashka.http-client/patch"></a>
``` clojure
Expand All @@ -210,7 +210,7 @@ Convenience wrapper for [`request`](#babashka.http-client/request) with method `
```

Convenience wrapper for [`request`](#babashka.http-client/request) with method `:patch`
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client.clj#L154-L161">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client.clj#L155-L162">Source</a></sub></p>

## <a name="babashka.http-client/post">`post`</a><a name="babashka.http-client/post"></a>
``` clojure
Expand All @@ -220,7 +220,7 @@ Convenience wrapper for [`request`](#babashka.http-client/request) with method `
```

Convenience wrapper for [`request`](#babashka.http-client/request) with method `:post`
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client.clj#L147-L152">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client.clj#L148-L153">Source</a></sub></p>

## <a name="babashka.http-client/put">`put`</a><a name="babashka.http-client/put"></a>
``` clojure
Expand All @@ -230,7 +230,7 @@ Convenience wrapper for [`request`](#babashka.http-client/request) with method `
```

Convenience wrapper for [`request`](#babashka.http-client/request) with method `:put`
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client.clj#L163-L170">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client.clj#L164-L171">Source</a></sub></p>

## <a name="babashka.http-client/request">`request`</a><a name="babashka.http-client/request"></a>
``` clojure
Expand All @@ -247,7 +247,9 @@ Perform request. Returns map with at least `:body`, `:status`
* `:headers` - a map of headers
* `:method` - the request method: `:get`, `:post`, `:head`, `:delete`, `:patch` or `:put`
* `:interceptors` - custom interceptor chain
* `:client` - a client as produced by [`client`](#babashka.http-client/client). If not provided a default client will be used.
* `:client` - a client as produced by [`client`](#babashka.http-client/client) or a clojure function. If not provided a default client will be used.
When providing :client with a a clojure function, it will be called with the Clojure representation of
the request which can be useful for testing.
* `:query-params` - a map of query params. The values can be a list to send multiple params with the same key.
* `:form-params` - a map of form params to send in the request body.
* `:body` - a file, inputstream or string to send as the request body.
Expand All @@ -259,8 +261,7 @@ Perform request. Returns map with at least `:body`, `:status`
* `:timeout` - request timeout in milliseconds
* `:throw` - throw on exceptional status codes, all other than `#{200 201 202 203 204 205 206 207 300 301 302 303 304 307}`
* `:version` - the HTTP version: `:http1.1` or `:http2`.

<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client.clj#L100-L124">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client.clj#L100-L125">Source</a></sub></p>

-----
# <a name="babashka.http-client.interceptors">babashka.http-client.interceptors</a>
Expand Down Expand Up @@ -293,15 +294,15 @@ Request: adds `:authorization` header based on `:basic-auth` (a map


Request: construct uri from map
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client/interceptors.clj#L220-L226">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client/interceptors.clj#L222-L228">Source</a></sub></p>

## <a name="babashka.http-client.interceptors/decode-body">`decode-body`</a><a name="babashka.http-client.interceptors/decode-body"></a>




Response: based on the value of `:as` in request, decodes as `:string`, `:stream` or `:bytes`. Defaults to `:string`.
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client/interceptors.clj#L208-L218">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client/interceptors.clj#L208-L220">Source</a></sub></p>

## <a name="babashka.http-client.interceptors/decompress-body">`decompress-body`</a><a name="babashka.http-client.interceptors/decompress-body"></a>

Expand All @@ -317,7 +318,7 @@ Response: decompresses body based on "content-encoding" header. Valid values: `


Default interceptor chain. Interceptors are called in order for request and in reverse order for response.
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client/interceptors.clj#L253-L264">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client/interceptors.clj#L256-L267">Source</a></sub></p>

## <a name="babashka.http-client.interceptors/form-params">`form-params`</a><a name="babashka.http-client.interceptors/form-params"></a>

Expand All @@ -333,7 +334,7 @@ Request: encodes `:form-params` map and adds `:body`.


Adds appropriate body and header if making a multipart request.
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client/interceptors.clj#L241-L251">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client/interceptors.clj#L244-L254">Source</a></sub></p>

## <a name="babashka.http-client.interceptors/oauth-token">`oauth-token`</a><a name="babashka.http-client.interceptors/oauth-token"></a>

Expand All @@ -358,13 +359,13 @@ Request: encodes `:query-params` map and appends to `:uri`.


Response: throw on exceptional status codes
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client/interceptors.clj#L231-L239">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client/interceptors.clj#L233-L242">Source</a></sub></p>

## <a name="babashka.http-client.interceptors/unexceptional-statuses">`unexceptional-statuses`</a><a name="babashka.http-client.interceptors/unexceptional-statuses"></a>



<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client/interceptors.clj#L228-L229">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/http-client/blob/main/src/babashka/http_client/interceptors.clj#L230-L231">Source</a></sub></p>

## <a name="babashka.http-client.interceptors/uri-with-query">`uri-with-query`</a><a name="babashka.http-client.interceptors/uri-with-query"></a>
``` clojure
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Babashka [http-client](https://github.com/babashka/http-client): HTTP client for Clojure and babashka built on java.net.http

## 0.4.18 (2024-04-18)

- Support a Clojure function as `:client` option, mostly useful for testing

## 0.4.17 (2024-04-12)

- [#49](https://github.com/babashka/http-client/issues/49): add `::oauth-token` interceptor
Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,17 @@ function is executed on the response. Default interceptors are in
configured on the level of requests by passing a modified `:interceptors`
chain.
#### Testing interceptors
For testing interceptors it can be useful to use the `:client` option in combination with a
Clojure function. When passing a function, the request won't be converted to a
`java.net.http.Request` but just passed as a ring request to the function. The
function is expected to return a ring response:
``` clojure
(http/get "https://clojure.org" {:client (fn [req] {:body 200})})
```
### Async
To execute request asynchronously, use `:async true`. The response will be a
Expand Down Expand Up @@ -375,7 +386,7 @@ Here is a code snippet for `deps.edn`
{:jvm-opts
[;; enable logging for java.net.http
"-Djdk.httpclient.HttpClient.log=errors,requests,headers,frames[:control:data:window:all..],content,ssl,trace,channel"]}
}}
}}
```
## Test
Expand Down
7 changes: 4 additions & 3 deletions src/babashka/http_client.clj
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@
* `:headers` - a map of headers
* `:method` - the request method: `:get`, `:post`, `:head`, `:delete`, `:patch` or `:put`
* `:interceptors` - custom interceptor chain
* `:client` - a client as produced by `client`. If not provided a default client will be used.
* `:client` - a client as produced by `client` or a clojure function. If not provided a default client will be used.
When providing :client with a a clojure function, it will be called with the Clojure representation of
the request which can be useful for testing.
* `:query-params` - a map of query params. The values can be a list to send multiple params with the same key.
* `:form-params` - a map of form params to send in the request body.
* `:body` - a file, inputstream or string to send as the request body.
Expand All @@ -118,8 +120,7 @@
* `:async-catch` - a function that is called on the async result if exceptional
* `:timeout` - request timeout in milliseconds
* `:throw` - throw on exceptional status codes, all other than `#{200 201 202 203 204 205 206 207 300 301 302 303 304 307}`
* `:version` - the HTTP version: `:http1.1` or `:http2`.
"
* `:version` - the HTTP version: `:http1.1` or `:http2`."
[opts]
(i/request opts))

Expand Down
15 changes: 9 additions & 6 deletions src/babashka/http_client/interceptors.clj
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,12 @@
:response (fn [resp]
(let [as (or (-> resp :request :as) :string)
body (:body resp)
body (case as
:string (slurp body)
:stream body
:bytes (stream-bytes body))]
body (if (not (string? body))
(case as
:string (slurp body)
:stream body
:bytes (stream-bytes body))
body)]
(assoc resp :body body)))})

(def construct-uri
Expand All @@ -232,11 +234,12 @@
"Response: throw on exceptional status codes"
{:name ::throw-on-exceptional-status-code
:response (fn [resp]
(let [status (:status resp)]
(if-let [status (:status resp)]
(if (or (false? (some-> resp :request :throw))
(contains? unexceptional-statuses status))
resp
(throw (ex-info (str "Exceptional status code: " status) resp)))))})
(throw (ex-info (str "Exceptional status code: " status) resp)))
resp))})

(def multipart
"Adds appropriate body and header if making a multipart request."
Expand Down
17 changes: 11 additions & 6 deletions src/babashka/http_client/internal.clj
Original file line number Diff line number Diff line change
Expand Up @@ -301,19 +301,24 @@
[{:keys [client raw] :as req}]
(let [client (or client @default-client)
request-defaults (:request client)
^HttpClient client (or (:client client) client)
client* (or (:client client) client)
^HttpClient client client*
ring-client (when (ifn? client*)
client*)
req (merge-with merge-opts request-defaults req)
req (update req :headers aux/prefer-string-keys)
request-interceptors (or (:interceptors req)
interceptors/default-interceptors)
req (apply-interceptors req request-interceptors :request)
req' (ring->HttpRequest req)
req' (when-not ring-client (ring->HttpRequest req))
async (:async req)
resp (if async
(.sendAsync client req' (HttpResponse$BodyHandlers/ofInputStream))
(.send client req' (HttpResponse$BodyHandlers/ofInputStream)))]
resp (if ring-client
(ring-client req)
(if async
(.sendAsync client req' (HttpResponse$BodyHandlers/ofInputStream))
(.send client req' (HttpResponse$BodyHandlers/ofInputStream))))]
(if raw resp
(let [resp (then resp response->map)
(let [resp (if ring-client resp (then resp response->map))
resp (then resp (fn [resp]
(assoc resp :request req)))
resp (reduce (fn [resp interceptor]
Expand Down
25 changes: 21 additions & 4 deletions test/babashka/http_client_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -501,16 +501,16 @@
(nil? (.getProtocols params)))))
(let [params (http/->SSLParameters {:ciphers ["SSL_NULL_WITH_NULL_NULL"]})]
(is (and (instance? javax.net.ssl.SSLParameters params)
(= (first (.getCipherSuites params)) "SSL_NULL_WITH_NULL_NULL"))))
(= "SSL_NULL_WITH_NULL_NULL" (first (.getCipherSuites params))))))
(let [params (http/->SSLParameters {:protocols ["TLSv1"]})]
(is (and (instance? javax.net.ssl.SSLParameters params)
(= (first (.getProtocols params)) "TLSv1"))))
(= "TLSv1" (first (.getProtocols params))))))
(let [params-from-opts (http/->SSLParameters {:ciphers ["SSL_NULL_WITH_NULL_NULL"]
:protocols ["TLSv1"]})
params-from-params (http/->SSLParameters params-from-opts)]
(is (and (instance? javax.net.ssl.SSLParameters params-from-params)
(= (first (.getCipherSuites params-from-params)) "SSL_NULL_WITH_NULL_NULL")
(= (first (.getProtocols params-from-params)) "TLSv1")))))
(= "SSL_NULL_WITH_NULL_NULL" (first (.getCipherSuites params-from-params)))
(= "TLSv1" (first (.getProtocols params-from-params)))))))

(deftest executor-test
(testing "nil passthrough"
Expand All @@ -534,6 +534,23 @@
(str (#'i/uri-with-query (java.net.URI. "https://borkdude:foobar@foobar.net:80/?q=1#/dude")
"q=%26moo"))))))

(deftest ring-client-test
(testing "inputstring body"
(doseq [resp [(http/get "https://clojure.org"
{:client (fn [_req]
{:body "Hello"
:clojure true})})
(http/get "https://clojure.org"
{:client (fn [req]
{:body (java.io.ByteArrayInputStream. (.getBytes "Hello"))
:clojure (= "https://clojure.org" (str (:uri req)))})})
(http/get "https://clojure.org"
{:client (fn [_req]
{:body (java.io.StringReader. "Hello")
:clojure true})})]]
(is (:clojure resp))
(is (= "Hello" (:body resp))))))

(comment
(run-server)
(stop-server))

0 comments on commit e7c4631

Please sign in to comment.