hato icon indicating copy to clipboard operation
hato copied to clipboard

Feature req: Muuntaja/pluggable encoding/decoding support

Open thenonameguy opened this issue 5 years ago • 6 comments

Hi! We would like to use https://github.com/metosin/muuntaja as we use metosin/jsonista for JSON encoding and decoding. Would it be possibly to switch out the current hard-coded content encoding library choices to a dynamic approach?

Either adopting muuntaja or defining protocols as extension points would be fine.

thenonameguy avatar Feb 18 '20 11:02 thenonameguy

I will take a look. Thanks for your interest!

gnarroway avatar Feb 19 '20 05:02 gnarroway

@thenonameguy The encode/decode already uses a multi method that dispatches on content type. Does providing your own implementation there work for you?

(defmethod hm/coerce-form-params :application/json
  [req]
  "{\"dummy\": 2}")

(hm/coerce-form-params {:content-type "application/json"
                        :form-params {:hello "world"}})

; => "{\"dummy\": 2}"

gnarroway avatar Mar 02 '20 10:03 gnarroway

Not really:

  1. Multimethods are slower that how Muuntaja implements it (I have perf-sensitive use-case)
  2. It handles more cases than form/query params, like content negotiation, which is hard to express with this interface

thenonameguy avatar Mar 11 '20 11:03 thenonameguy

Okay @thenonameguy , my original interpretation was simply that you wanted to replace the Cheshire optional dependency with something you already depended on.

Given hato is middleware based, it is easy to adapt the middleware chain to do what you want, and to exclude irrelevant pieces.

Below is a small example that encodes :form-params you send into the request body, and decodes the response body back into the same body field (i.e. retains the contract that hato uses). Note that as I reused high level muuntaja code, the response/request is backwards (because muuntaja has the perspective of the web server rather than client).

Let me know if this works for you.

(ns muuntaja-client
  (:require [hato.middleware :as hm]
            [hato.client :as hc]
            [muuntaja.middleware :as mm]
            [muuntaja.core :as mc]))

;; Create re-usable client
(def client (hc/build-http-client {}))

;; Hit a json endpoint without a serializer returns a string
(hc/get "https://httpbin.org/json" {:http-client client :as :json})
; => {...:body "{...}"}

;; Create a muuntaja instance
(def my-mun (mc/create mc/default-options))

;; Transform clojure :body into json :body
(defn- muuntaja-request
  [req]
  (mc/format-response my-mun req
                      (assoc req
                        :muuntaja/response (mc/response-format my-mun req)
                        :body (:form-params req))))

;; Transform json :body into clojure :body
(defn- muuntaja-response
  [_ resp]
  (let [r (mc/format-request my-mun (assoc resp :muuntaja/request (mc/request-format my-mun resp)))]
    (-> r 
        (assoc :body (or (:body-params r) (:body r)))
        (dissoc :body-params))))

;; Wrap the transformers in middleware
(defn wrap-format
  [client]
  (fn
    ([req]
     (muuntaja-response req (client (muuntaja-request req))))
    ([req respond raise]
     (client (muuntaja-request req) #(respond (muuntaja-response req %)) raise))))

;; Modify the default middleware chain:
;; - replace wrap-form-params and wrap-outout-coercion with wrap-format (which does input and output)
(def my-middleware [hm/wrap-request-timing
                    hm/wrap-query-params
                    hm/wrap-basic-auth
                    hm/wrap-oauth
                    hm/wrap-user-info
                    hm/wrap-url
                    hm/wrap-decompression

                    wrap-format
                    ;hm/wrap-output-coercion
                    hm/wrap-exceptions
                    hm/wrap-accept
                    hm/wrap-accept-encoding
                    hm/wrap-multipart
                    hm/wrap-content-type

                    ;hm/wrap-form-params
                    hm/wrap-nested-params
                    hm/wrap-method])

;; Create your new client
(def my-request (hm/wrap-request hc/request* my-middleware))

; Add your own convenience methods if you desire
(defn my-get
  [url opts]
  (my-request (merge opts {:url url :method :get})))

;; Now the form-params are sent as json body, and response returned as clojure
(my-get "https://httpbin.org/json" {:http-client client :as :json :form-params {:kikka 42}})
;; => {...:body {}}

gnarroway avatar Mar 23 '20 06:03 gnarroway

Yep that sound much closer to what I envisioned. The middleware support fixes the issue's need for me, now the question is does hato want to include this (optional) support like it does for cheshire or we leave this to users to decide. If you decide the latter, we should get this code checked in to git somewhere so it can be used as library code or documentation.

thenonameguy avatar Mar 23 '20 09:03 thenonameguy

Hi, I had to patch muuntaja first and it all took a little while, so I did not see the answers to this thread. However, you might want to have a look at #10 .

chrisbetz avatar Mar 24 '20 14:03 chrisbetz