dakrone / clj-http

An idiomatic clojure http client wrapping the apache client. Officially supported version.
http://clojars.org/clj-http
MIT License
1.77k stars 408 forks source link

option to pre-process `:query-params` #642

Open raymcdermott opened 6 months ago

raymcdermott commented 6 months ago

When making API calls to an external provider, we need to have the query parameters in camelCase rather than our beloved kebab-case 😢

To achieve this we have a small function which, as you can imagine, works fine.

(def kebab->camel (partial cske/transform-keys csk/->camelCaseString))

(let [api-query-params (kebab->camel query-params)
       request-opts (assoc arguments :query-params api-query-params)
       results (client request-opts)]
...

It would be nice if there was an option to automate this on the client since I imagine that this is a fairly common requirement.

Would you accept a PR? Must the PR avoid the extra dependency on camel-snake-kebab?

kpassapk commented 3 months ago

Maybe this could be done through middleware? And added to the list of contributed middleware in this repo.

fr33m0nk commented 1 month ago

I had a similar need but for :body. query-string mw definitely is a bit mouthful as :query-string field contains value as string e.g. if {:query-params {:user-id 1 :teaId 2}} then :query-string is "user-id=1&teaId=2".

(require '[clj-http.client :as clj-http] '[camel-snake-kebab.core :as csk] '[camel-snake-kebab.extras :as cske])

;; a lengthy yet trivial middleware
(defn- encode-qs-mw
  "a Middleware for encoding query params."
  [client]
  (let [qs-kebab->camel
        (fn [req]
          (if-let [qs (:query-string req)]
            (assoc req :query-string (->> (str/split qs #"&")
                                          (map (comp
                                                 (partial str/join "=")
                                                 (juxt (comp csk/->camelCaseString first) second)
                                                 #(str/split % #"=")))
                                          (str/join "&")))
            req))]
    (fn
      ([req]
       (client (qs-kebab->camel req)))
      ([req respond raise]
       (client (qs-kebab->camel req)
               respond
               raise)))))

In action:


(clj-http/with-additional-middleware
  [encode-qs-mw]
  (clj-http/get "https://jsonplaceholder.typicode.com/posts" 
                {:debug true
                 :query-params {:user-id 1 :teaId 2}}))

;;=> 
;; Request logs

;; Request: nil
;;{:user-info nil,
;; :use-header-maps-in-response? true,
;; :body-type nil,
;; :debug true,
;; :headers {"accept-encoding" "gzip, deflate"},
;; :server-port nil,
;; :url "https://jsonplaceholder.typicode.com/posts",
;; :flatten-nested-keys (:query-params),
;; :uri "/posts",
;; :server-name "jsonplaceholder.typicode.com",
;; :query-string "userId=1&teaId=2",   ;; <<-- what was intended
;; :body nil,
;; :scheme :https,
;; :request-method :get}
;;HttpRequest:
;;{:config nil,
;; :method "GET",
;; :requestLine
;; #object[org.apache.http.message.BasicRequestLine 0x6a9ff8e9 "GET https://jsonplaceholder.typicode.com/posts?userId=1&teaId=2 HTTP/1.1"],
;; :aborted false,
;; :params
;; #object[org.apache.http.params.BasicHttpParams 0x74f2986c "[parameters={}]"],
;; :protocolVersion
;; #object[org.apache.http.HttpVersion 0x7aa51a2a "HTTP/1.1"],
;; :URI
;; #object[java.net.URI 0x6d630e1f "https://jsonplaceholder.typicode.com/posts?userId=1&teaId=2"],
;; :class org.apache.http.client.methods.HttpGet,
;; :allHeaders
;; [#object[org.apache.http.message.BasicHeader 0x1b574644 "Connection: close"],
;;  #object[org.apache.http.message.BasicHeader 0x29c2be0d "accept-encoding: gzip, deflate"]]}