Skip to content

Commit

Permalink
Merge branch 'develop' into muuntaja-based-content-encoding-decoding
Browse files Browse the repository at this point in the history
  • Loading branch information
Dr. Christian Betz committed Sep 12, 2022
2 parents 3d130b2 + 4db081c commit 89d2b2e
Show file tree
Hide file tree
Showing 11 changed files with 626 additions and 532 deletions.
21 changes: 6 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ request and returns a response. Convenience wrappers are provided for the http v
; Convenience wrappers
(hc/get "https://httpbin.org/get")
(hc/get "https://httpbin.org/get" {:as :json})
(hc/post "https://httpbin.org/post" {:body "{\"a\": 1}" :content-type :json})
(hc/post "https://httpbin.org/post" {:body {:a 1} :content-type :json})
```

#### request options
Expand Down Expand Up @@ -318,10 +318,10 @@ As a convenience, nesting can also be controlled by `:flatten-nested-keys`:

### Output coercion

You can control whether you like hato to return an `InputStream` (using `:as :stream`), `byte-array` (using `:as :byte-array`) or `String` (`:as :string`) with no further coercion.
You can control whether you like hato to return an `InputStream` (using `:as :stream`), `byte-array` (using `:as :byte-array`) or `String` (`:as :string`) with no further coercion. The default is to parse data based upon the `Content-Type` header based upon [muuuntaja](https://github.com/gorillalabs/muuntaja), by default supporting EDN, JSON, Transit, Msgpack, Text.

```clojure
; Returns a string response
; Returns a response parsed by muuntaja based upon content-type header.
(hc/get "http://moo.com" {})

; Returns a byte array
Expand All @@ -330,23 +330,14 @@ You can control whether you like hato to return an `InputStream` (using `:as :st
; Returns an InputStream
(hc/get "http://moo.com" {:as :stream})

; Coerces clojure strings
; Coerces clojure strings (shouldn't be necessary as application/edn content type will be automatically converted)
(hc/get "http://moo.com" {:as :clojure})

; Coerces transit. Requires optional dependency com.cognitect/transit-clj.
(hc/get "http://moo.com" {:as :transit+json})
(hc/get "http://moo.com" {:as :transit+msgpack})

; Coerces JSON strings into clojure data structure
; Requires optional dependency cheshire
(hc/get "http://moo.com" {:as :json})
(hc/get "http://moo.com" {:as :json-string-keys})

; Coerce responses with exceptional status codes
(hc/get "http://moo.com" {:as :json :coerce :always})
(hc/get "http://moo.com" {:coerce :always})
```

By default, hato only coerces JSON responses for unexceptional statuses. Control this with the `:coerce` option:
Hato only coerces (default parsed) responses for unexceptional statuses. Control this with the `:coerce` option:

```clojure
:unexceptional ; default - only coerce response bodies for unexceptional status codes
Expand Down
16 changes: 8 additions & 8 deletions deps.edn
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{:paths ["src"]
:deps {gorillalabs/muuntaja {:mvn/version "0.8.0"}}
:aliases {:test {:extra-paths ["test"]
:extra-deps {ring/ring-core {:mvn/version "1.9.5"}
javax.servlet/servlet-api {:mvn/version "2.5"}
funcool/promesa {:mvn/version "8.0.446"}
cheshire/cheshire {:mvn/version "5.10.2"}
com.cognitect/transit-clj {:mvn/version "0.8.319"}
http-kit/http-kit {:mvn/version "2.6.0"}
io.github.cognitect-labs/test-runner
{:git/tag "v0.5.0" :git/sha "48c3c67"}}
:extra-deps {ring/ring-core {:mvn/version "1.9.5"}
javax.servlet/servlet-api {:mvn/version "2.5"}
funcool/promesa {:mvn/version "8.0.446"}
cheshire/cheshire {:mvn/version "5.10.2"}
com.cognitect/transit-clj {:mvn/version "0.8.319"}
http-kit/http-kit {:mvn/version "2.6.0"}
io.github.cognitect-labs/test-runner {:git/tag "v0.5.0" :git/sha "48c3c67"}}
:exec-fn cognitect.test-runner.api/test}}}
122 changes: 61 additions & 61 deletions src/hato/client.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@
"Core implementation of an HTTP client wrapping JDK11's java.net.http.HttpClient."
(:refer-clojure :exclude [get])
(:require
[clojure.string :as str]
[hato.middleware :as middleware]
[clojure.java.io :as io])
[clojure.string :as str]
[hato.middleware :as middleware]
[clojure.java.io :as io])
(:import
(java.net.http
HttpClient$Redirect
HttpClient$Version
HttpResponse$BodyHandlers
HttpRequest$BodyPublisher
HttpRequest$BodyPublishers HttpResponse HttpClient HttpRequest HttpClient$Builder HttpRequest$Builder)
(java.net CookiePolicy CookieManager URI ProxySelector Authenticator PasswordAuthentication CookieHandler)
(javax.net.ssl KeyManagerFactory TrustManagerFactory SSLContext X509TrustManager TrustManager)
(java.security KeyStore SecureRandom)
(java.time Duration)
(java.util.function Function Supplier)
(java.io File InputStream)
(clojure.lang ExceptionInfo)
(java.security.cert X509Certificate)))
(java.net.http
HttpClient$Redirect
HttpClient$Version
HttpResponse$BodyHandlers
HttpRequest$BodyPublisher
HttpRequest$BodyPublishers HttpResponse HttpClient HttpRequest HttpClient$Builder HttpRequest$Builder)
(java.net CookiePolicy CookieManager URI ProxySelector Authenticator PasswordAuthentication CookieHandler)
(javax.net.ssl KeyManagerFactory TrustManagerFactory SSLContext X509TrustManager TrustManager)
(java.security KeyStore SecureRandom)
(java.time Duration)
(java.util.function Function Supplier)
(java.io File InputStream)
(clojure.lang ExceptionInfo)
(java.security.cert X509Certificate)))

(defn- ->Authenticator
[v]
Expand Down Expand Up @@ -187,10 +187,10 @@
(defn- with-headers
^HttpRequest$Builder [builder headers]
(reduce-kv
(fn [^HttpRequest$Builder b ^String hk ^String hv]
(.header b hk hv))
builder
headers))
(fn [^HttpRequest$Builder b ^String hk ^String hv]
(.header b hk hv))
builder
headers))

(defn- with-authenticator
^HttpClient$Builder [^HttpClient$Builder b a]
Expand Down Expand Up @@ -231,17 +231,17 @@
ssl-parameters
version]}]
(cond-> (HttpClient/newBuilder)
connect-timeout (.connectTimeout (Duration/ofMillis connect-timeout))
executor (.executor executor)
redirect-policy (.followRedirects (->Redirect redirect-policy))
priority (.priority priority)
proxy (.proxy (->ProxySelector proxy))
version (.version (->Version version))
ssl-context (.sslContext (->SSLContext ssl-context))
ssl-parameters (.sslParameters ssl-parameters)
authenticator (with-authenticator authenticator)
(or cookie-handler cookie-policy) (with-cookie-handler cookie-handler cookie-policy)
true .build))
connect-timeout (.connectTimeout (Duration/ofMillis connect-timeout))
executor (.executor executor)
redirect-policy (.followRedirects (->Redirect redirect-policy))
priority (.priority priority)
proxy (.proxy (->ProxySelector proxy))
version (.version (->Version version))
ssl-context (.sslContext (->SSLContext ssl-context))
ssl-parameters (.sslParameters ssl-parameters)
authenticator (with-authenticator authenticator)
(or cookie-handler cookie-policy) (with-cookie-handler cookie-handler cookie-policy)
true .build))

(defn ^HttpRequest ring-request->HttpRequest
"Creates an HttpRequest from a ring request map.
Expand Down Expand Up @@ -274,19 +274,19 @@
:or {request-method :get}
:as req}]
(cond-> (HttpRequest/newBuilder
(URI. (str (name scheme)
"://"
server-name
(some->> server-port (str ":"))
uri
(some->> query-string (str "?")))))
expect-continue (.expectContinue expect-continue)
version (.version (->Version version))
headers (with-headers headers)
timeout (.timeout (Duration/ofMillis timeout))
true (-> (.method (str/upper-case (name request-method))
(->BodyPublisher req))
.build)))
(URI. (str (name scheme)
"://"
server-name
(some->> server-port (str ":"))
uri
(some->> query-string (str "?")))))
expect-continue (.expectContinue expect-continue)
version (.version (->Version version))
headers (with-headers headers)
timeout (.timeout (Duration/ofMillis timeout))
true (-> (.method (str/upper-case (name request-method))
(->BodyPublisher req))
.build)))

(defn request*
[{:keys [http-client async?]
Expand All @@ -297,26 +297,26 @@
(if-not async?
(let [resp (.send http-client http-request bh)]
(response-map
{:request req
:http-client http-client
:response resp}))
{:request req
:http-client http-client
:response resp}))

(-> (.sendAsync http-client http-request bh)
(.thenApply
(reify Function
(apply [_ resp]
(respond
(response-map
{:request req
:http-client http-client
:response resp})))))
(reify Function
(apply [_ resp]
(respond
(response-map
{:request req
:http-client http-client
:response resp})))))
(.exceptionally
(reify Function
(apply [_ e]
(let [cause (.getCause ^Exception e)]
(if (instance? ExceptionInfo cause)
(raise cause)
(raise e))))))))))
(reify Function
(apply [_ e]
(let [cause (.getCause ^Exception e)]
(if (instance? ExceptionInfo cause)
(raise cause)
(raise e))))))))))

(defn request
[req & [respond raise]]
Expand Down
8 changes: 4 additions & 4 deletions src/hato/conversion.clj
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,16 @@
;;; Decoding

(defmulti decode
"Extensible content-type based decoder."
(fn [resp _] (:content-type resp)))
"Extensible content-type based decoder."
(fn [resp _] (:content-type resp)))

(defmethod decode :default
[{:keys [content-type] :as resp} _]
; Throw for types that we would support if dependencies existed.
(when (#{:application/json :application/transit+json :application/transit+msgpack} content-type)
(throw (IllegalArgumentException.
(format "Unable to decode content-type %s. Add optional dependencies or provide alternative decoder."
(:content-type resp)))))
(format "Unable to decode content-type %s. Add optional dependencies or provide alternative decoder."
(:content-type resp)))))

; Return strings for text, or the original result otherwise.
(if (= "text" (and content-type (namespace content-type)))
Expand Down
33 changes: 33 additions & 0 deletions src/hato/format/text.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
(ns hato.format.text
(:refer-clojure :exclude [format])
(:require [clojure.edn :as edn]
[muuntaja.format.core :as core])
(:import (java.io InputStreamReader PushbackReader InputStream OutputStream)))

(defn decoder [options]
(reify
core/Decode
(decode [_ data charset]
(slurp (InputStreamReader. ^InputStream data ^String charset)))))

(defn encoder [_]
(reify
core/EncodeToBytes
(encode-to-bytes [_ data charset]
(.getBytes
(str data)
^String charset))
core/EncodeToOutputStream
(encode-to-output-stream [_ data charset]
(fn [^OutputStream output-stream]
(.write output-stream (.getBytes
(str data)
^String charset))))))

(def generic
(core/map->Format
{:name "text/*"
:matches #"^text/(.+)$"
:decoder [decoder]
:encoder [encoder]}))

Loading

0 comments on commit 89d2b2e

Please sign in to comment.