I totally understand that, I felt the same way first coming to Clojure.
For this situation, there are a couple approaches that you could take
depending on how hardcore you want to be about keeping things functional
& immutable.
The most similar thing you could do to your Java code would be keeping
the token in an atom inside of a connection record.
(defrecord MyAuthHttpClient [token user psw])
(defn my-auth-http-client [usr psw]
(let [token (atom nil)]
(maybe-update-token token usr psw)
(->MyAuthHttpClient token usr psw)))
(defn get-response [client url]
(maybe-update-token (:token client) (:user client) (:psw client))
(let [token @(:token client)]
(do-request token url)))
If you want to do the same thing, but are uncomfortable storing username
and password in the record, you can close them into a function:
(defrecord MyAuthHttpClient [token refresh-token])
(defn my-auth-http-client [usr psw]
(let [refresh-token (fn [t] (maybe-update-token t usr psw))
token (atom nil)]
(refresh-token token)
(->MyAuthHttpClient token refresh-token)))
(defn get-response [client url]
((:refresh-token client) (:token client))
(let [token @(:token client)]
(do-request token url)))
If you want polymorphism, such that you can have many authentication schemes
for different HttpClients, and insulate your calling code from those
details,
use a protocol:
(defprotocol Client
(get-response [this url] "Perform an HTTP GET against `url`."))
(defrecord NoAuthClient []
Client
(get-response [this url] (slurp url)))
;; E.g.
(get-response (->NoAuthClient) "https://blockchain.info/stats?format=json";
)
;=> "{\"timestamp\":1.589365823E12,\"market_price_usd\":8920.2,..."
(defrecord MyAuthHttpClient [token refresh-token]
(get-response [this url]
(refresh-token token)
(do-request token url)))
So far, this has all been pretty a pretty typical OO style of abstraction &
separation of concerns. For certain problems, this is really effective.
Now let's imagine that your application grows a lot, you have a dozen
different http clients, and they're all hitting endpoints with different
rate
limits many times per second. You want to add a rate limiting layer,
simplify
logging of all of the requests / responses, and make the interface
asynchronous for callers so that they're not blocking for every response.
In that case (in my option), the most effective thing is to make your
client
more functional — it shouldn't keep state, perform side effects, etc. All
of
that would happen at a higher level of your code, and give you the
flexibility
to combine different clients with the rate limit layer and the async request
code however you want.
Instead, you client would return information about the requests that it
needs
made. This is a stronger abstraction with stronger separation of concerns —
the request generation & signing code don't care how or when the requests
are
executed, the requesting code doesn't care how requests are built or signed,
and the highest-level code only knows that it made a request — but it's
significantly more work to define something that precisely.
E.g. you might have:
(defprotocol Client
(request [this request-data]
"Build a request and return `this`.")
(response [this response-data]
"Return a possibly updated `this` for a response to one of the
client's requests.")
(consume-requests [this]
"Consume all queued requests, return [this requests]."))
;; The NoAuthClient doesnt do much...
(defrecord NoAuthClient [queued]
Client
(request [this request-data] (assoc this :queued (conj queued request-
data)))
(response [this _] this)
(consume-requests [this] [(assoc this :queued []) queued]))
;; Your token auth client, on the other hand...
(defrecord MyAuthHttpClient
[token refresh-token requests queued-for-after-refresh]
Client
(request [this request-data]
;; It is not actually making any requests to refresh, just
;; figuring out _what_ needs to be done, and letting the calling
;; code take care of _how_ to do it (via clj-http?, aleph?, right
;; away?, later?, etc)
(if (token-expired-given-current-time? token (:ts request-data))
(-> this
(update :queued-for-after-refresh conj request-data)
(update :requests conj (refresh-token token)))
(update this :requests conj request-data)))
(response [this response-data]
(if (is-token-refresh-response? response-data)
(let [new-token (parse-new-token response-data)
;; Any requests that were awaiting a new token are
;; now ready, but probably need to have the token
;; included
insert (map #(assoc % :token new-token))
requests (into requests insert queued-for-after-refresh)]
(assoc this :token n