I'd try to separate the "I/O or side-effecting" parts from the "purely data processing" parts. This makes the program much easier to test --- the "purer" the code, the better it is. This also helps tease apart domain-agnostic parts from domain-specialised parts, which is useful, because domain-agnostic parts tend to be generalisable and thus more reusable.
I've taken the liberty to rearrange the code, and rename functions to illustrate what I mean.
(defn pipeline-api-endpoint ;; lifted out of `fetch-pipeline`
[project-name]
;; knows how to translate, or perhaps more generally, map, a project name to a project URL format
(defn http-get-basic-auth ;; instead of `fetch-pipeline', because now this operation doesn't care about a specific type of URL
[well-formed-url] ;; it can simply assume someone gives it a well-formed URL.
(client/get well-formed-url
{:basic-auth "username:password"})) ;; we'll see about this hard-coding later
(defn pipeline-build-count ;; now this only cares about looking up build count, so no "GET" semantics
;; assumes it gets a well-formed response
[{:keys [body] :as response}] ;; destructuring for convenience and function API documentation
(-> body
(parse-string true)
:pipelines
first
:counter))
(defn fetch-pipeline-counts! ;; ties all the pieces together
[project-names]
(reduce (fn [builds project-name]
(conj builds
(-> project-name
pipeline-api-endpoint
http-get-basic-auth
pipeline-build-count)))
[]
project-names))
Now... It turns out that fetch-pipeline-counts! is a giant effectful process, tied directly to http-get-basic-auth. We could try to lift out the effectful part, and try to make it a pure function.
(defn http-get-basic-auth
[well-formed-url username password]
(client/get well-formed-url
{:basic-auth (str username ":" password)}))
(defn http-basic-auth-getter
"Given basic auth credentials, return a function that takes an HTTP endpoint, and GETs data from there."
[username password]
(fn [well-formed-url]
(http-get-basic-auth well-formed-url
username
password)))
(defn fetch-pipeline-counts-alt
[pipeline-fetcher project-names]
;; Easier to unit test. We can pass a mock fetcher that doesn't call over the network.
;; In fact, we can now use any kind of "fetcher", even read from a DB or file where we may have dumped raw GET results.
(reduce (fn [builds project-name]
(conj builds
(-> project-name
pipeline-api-endpoint
pipeline-fetcher
pipeline-build-count)))
[]
project-names))
(comment
(fetch-pipeline-counts-alt (http-basic-auth-getter "username" "password")
["projectA"
"projectB"
"projectC"
"projectD"])
)
A closer look might suggest that we're now describing processes that could be much more general than fetching pipeline counts from an HTTP endpoint...
Enjoy Clojuring! :)