Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content-negotiation using muuntaja #10

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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