Add authentications support to Clojure client

Closes #1654
This commit is contained in:
xhh 2015-12-03 13:25:59 +08:00
parent 330053902e
commit afb7e31e21
10 changed files with 241 additions and 89 deletions

View File

@ -15,5 +15,6 @@
<#hasOptionalParams> </hasOptionalParams> :form-params {<#formParams>"<baseName>" <paramName> </formParams>}<#bodyParam>
<#hasOptionalParams> </hasOptionalParams> :body-param <paramName></bodyParam>
<#hasOptionalParams> </hasOptionalParams> :content-types [<#consumes>"<mediaType>"<#hasMore> </hasMore></consumes>]
<#hasOptionalParams> </hasOptionalParams> :accepts [<#produces>"<mediaType>"<#hasMore> </hasMore></produces>]}))<#hasOptionalParams>)</hasOptionalParams>
<#hasOptionalParams> </hasOptionalParams> :accepts [<#produces>"<mediaType>"<#hasMore> </hasMore></produces>]
<#hasOptionalParams> </hasOptionalParams> :auth-names [<#authMethods>"<&name>"<#hasMore> </hasMore></authMethods>]})<#hasOptionalParams>)</hasOptionalParams>)
</operation></operations>

View File

@ -1,4 +1,4 @@
(ns {{{baseNamespace}}}.core
{{=< >=}}(ns <&baseNamespace>.core
(:require [cheshire.core :refer [generate-string parse-string]]
[clojure.string :as str]
[clj-http.client :as client])
@ -7,12 +7,18 @@
(java.util Date TimeZone)
(java.text SimpleDateFormat)))
(def auth-definitions
{<#authMethods>"<&name>" <#isBasic>{:type :basic}</isBasic><#isApiKey>{:type :api-key<#isKeyInHeader> :in :header</isKeyInHeader><#isKeyInQuery> :in :query</isKeyInQuery> :param-name "<&keyParamName>"}</isApiKey><#isOAuth>{:type :oauth2}</isOAuth><#hasMore>
</hasMore></authMethods>})
(def default-api-context
"Default API context."
{:base-url "http://petstore.swagger.io/v2"
{:base-url "<&basePath>"
:date-format "yyyy-MM-dd"
:datetime-format "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"
:debug false})
:debug false
:auths {<#authMethods>"<&name>" nil<#hasMore>
</hasMore></authMethods>}})
(def ^:dynamic *api-context*
"Dynamic API context to be applied in API calls."
@ -20,12 +26,16 @@
(defmacro with-api-context
"A helper macro to wrap *api-context* with default values."
[context & body]
`(binding [*api-context* (merge *api-context* ~context)]
~@body))
[api-context & body]
`(let [api-context# ~api-context
api-context# (-> *api-context*
(merge api-context#)
(assoc :auths (merge (:auths *api-context*) (:auths api-context#))))]
(binding [*api-context* api-context#]
~@body)))
(defmacro check-required-params
"Throw exception if the given parameter value is nil."
"Throw exception if any of the given parameters is nil."
[& params]
(->> params
(map (fn [p]
@ -33,9 +43,9 @@
(throw (IllegalArgumentException. ~(str "The parameter \"" p "\" is required"))))))
(list* 'do)))
(defn- make-date-format
([format-str] (make-date-format format-str nil))
([format-str time-zone]
(defn- ^SimpleDateFormat make-date-format
([^String format-str] (make-date-format format-str nil))
([^String format-str ^String time-zone]
(let [date-format (SimpleDateFormat. format-str)]
(when time-zone
(.setTimeZone date-format (TimeZone/getTimeZone time-zone)))
@ -75,18 +85,45 @@
(-> (make-date-format datetime-format time-zone)
(.parse s)))))
(defn param-to-str [param]
(defn param->str
"Format the given parameter value to string."
[param]
(cond
(instance? Date param) (format-datetime param)
(sequential? param) (str/join "," param)
:else (str param)))
(defn auth->opts
"Process the given auth to an option map that might conatin request options and parameters."
[{:keys [type in param-name]} value]
(case type
:basic {:req-opts {:basic-auth value}}
:oauth2 {:req-opts {:oauth-token value}}
:api-key (case in
:header {:header-params {param-name value}}
:query {:query-params {param-name value}}
(throw (IllegalArgumentException. (str "Invalid `in` for api-key auth: " in))))
(throw (IllegalArgumentException. (str "Invalid auth `type`: " type)))))
(defn process-auth
"Process the given auth name into options, which is merged into the given opts."
[opts auth-name]
(if-let [value (get-in *api-context* [:auths auth-name])]
(merge-with merge
opts
(auth->opts (get auth-definitions auth-name) value))
opts))
(defn auths->opts
"Process the given auth names to an option map that might conatin request options and parameters."
[auth-names]
(reduce process-auth {} auth-names))
(defn make-url
"Make full URL by adding base URL and filling path parameters."
[path path-params]
(let [path (reduce (fn [p [k v]]
(str/replace p (re-pattern (str "\\{" k "\\}")) (param-to-str v)))
(str/replace p (re-pattern (str "\\{" k "\\}")) (param->str v)))
path
path-params)]
(str (:base-url *api-context*) path)))
@ -95,15 +132,15 @@
"Normalize parameter value, handling three cases:
for sequential value, normalize each elements of it;
for File value, do nothing with it;
otherwise, call `param-to-string`."
otherwise, call `param->str`."
[param]
(cond
(sequential? param) (map normalize-param param)
(instance? File param) param
:else (param-to-str param)))
:else (param->str param)))
(defn normalize-params
"Normalize parameters values: remove nils, format to string with `param-to-str`."
"Normalize parameters values: remove nils, format to string with `param->str`."
[params]
(->> params
(remove (comp nil? second))
@ -144,7 +181,7 @@
;; for non-JSON response, return the body string directly
:else body))
(defn form-params-to-multipart
(defn form-params->multipart
"Convert the given form parameters map into a vector as clj-http's :multipart option."
[form-params]
(->> form-params
@ -153,25 +190,27 @@
(defn call-api
"Call an API by making HTTP request and return its response."
[path method {:keys [path-params query-params header-params form-params body-param content-types accepts]}]
[path method {:keys [path-params body-param content-types accepts auth-names] :as opts}]
(let [{:keys [debug]} *api-context*
{:keys [req-opts query-params header-params form-params]} (auths->opts auth-names)
query-params (merge query-params (:query-params opts))
header-params (merge header-params (:header-params opts))
form-params (merge form-params (:form-params opts))
url (make-url path path-params)
content-type (or (json-preferred-mime content-types)
(and body-param :json))
content-type (or (json-preferred-mime content-types) (and body-param :json))
accept (or (json-preferred-mime accepts) :json)
multipart? (= "multipart/form-data" content-type)
opts (cond-> {:url url :method method}
accept (assoc :accept accept)
(seq query-params) (assoc :query-params (normalize-params query-params))
(seq header-params) (assoc :header-params (normalize-params header-params))
(and content-type (not multipart?)) (assoc :content-type content-type)
multipart? (assoc :multipart (-> form-params
normalize-params
form-params-to-multipart))
(and (not multipart?) (seq form-params)) (assoc :form-params (normalize-params form-params))
body-param (assoc :body (serialize body-param content-type))
debug (assoc :debug true :debug-body true))
resp (client/request opts)]
req-opts (cond-> req-opts
true (assoc :url url :method method)
accept (assoc :accept accept)
(seq query-params) (assoc :query-params (normalize-params query-params))
(seq header-params) (assoc :headers (normalize-params header-params))
(and content-type (not multipart?)) (assoc :content-type content-type)
multipart? (assoc :multipart (-> form-params normalize-params form-params->multipart))
(and (not multipart?) (seq form-params)) (assoc :form-params (normalize-params form-params))
body-param (assoc :body (serialize body-param content-type))
debug (assoc :debug true :debug-body true))
resp (client/request req-opts)]
(when debug
(println "Response:")
(println resp))

View File

@ -14,7 +14,8 @@
:form-params {}
:body-param body
:content-types ["application/json" "application/xml"]
:accepts ["application/json" "application/xml"]})))
:accepts ["application/json" "application/xml"]
:auth-names ["petstore_auth"]})))
(defn add-pet
"Add a new pet to the store
@ -28,7 +29,8 @@
:form-params {}
:body-param body
:content-types ["application/json" "application/xml"]
:accepts ["application/json" "application/xml"]})))
:accepts ["application/json" "application/xml"]
:auth-names ["petstore_auth"]})))
(defn find-pets-by-status
"Finds Pets by status
@ -41,7 +43,8 @@
:query-params {"status" status }
:form-params {}
:content-types []
:accepts ["application/json" "application/xml"]})))
:accepts ["application/json" "application/xml"]
:auth-names ["petstore_auth"]})))
(defn find-pets-by-tags
"Finds Pets by tags
@ -54,7 +57,8 @@
:query-params {"tags" tags }
:form-params {}
:content-types []
:accepts ["application/json" "application/xml"]})))
:accepts ["application/json" "application/xml"]
:auth-names ["petstore_auth"]})))
(defn get-pet-by-id
"Find pet by ID
@ -66,7 +70,8 @@
:query-params {}
:form-params {}
:content-types []
:accepts ["application/json" "application/xml"]}))
:accepts ["application/json" "application/xml"]
:auth-names ["api_key"]}))
(defn update-pet-with-form
"Updates a pet in the store with form data
@ -79,7 +84,8 @@
:query-params {}
:form-params {"name" name "status" status }
:content-types ["application/x-www-form-urlencoded"]
:accepts ["application/json" "application/xml"]})))
:accepts ["application/json" "application/xml"]
:auth-names ["petstore_auth"]})))
(defn delete-pet
"Deletes a pet
@ -92,7 +98,8 @@
:query-params {}
:form-params {}
:content-types []
:accepts ["application/json" "application/xml"]})))
:accepts ["application/json" "application/xml"]
:auth-names ["petstore_auth"]})))
(defn upload-file
"uploads an image
@ -105,4 +112,5 @@
:query-params {}
:form-params {"additionalMetadata" additional-metadata "file" file }
:content-types ["multipart/form-data"]
:accepts ["application/json" "application/xml"]})))
:accepts ["application/json" "application/xml"]
:auth-names ["petstore_auth"]})))

View File

@ -12,7 +12,8 @@
:query-params {}
:form-params {}
:content-types []
:accepts ["application/json" "application/xml"]}))
:accepts ["application/json" "application/xml"]
:auth-names ["api_key"]}))
(defn place-order
"Place an order for a pet
@ -26,7 +27,8 @@
:form-params {}
:body-param body
:content-types []
:accepts ["application/json" "application/xml"]})))
:accepts ["application/json" "application/xml"]
:auth-names []})))
(defn get-order-by-id
"Find purchase order by ID
@ -38,7 +40,8 @@
:query-params {}
:form-params {}
:content-types []
:accepts ["application/json" "application/xml"]}))
:accepts ["application/json" "application/xml"]
:auth-names []}))
(defn delete-order
"Delete purchase order by ID
@ -50,4 +53,5 @@
:query-params {}
:form-params {}
:content-types []
:accepts ["application/json" "application/xml"]}))
:accepts ["application/json" "application/xml"]
:auth-names []}))

View File

@ -14,7 +14,8 @@
:form-params {}
:body-param body
:content-types []
:accepts ["application/json" "application/xml"]})))
:accepts ["application/json" "application/xml"]
:auth-names []})))
(defn create-users-with-array-input
"Creates list of users with given input array
@ -28,7 +29,8 @@
:form-params {}
:body-param body
:content-types []
:accepts ["application/json" "application/xml"]})))
:accepts ["application/json" "application/xml"]
:auth-names []})))
(defn create-users-with-list-input
"Creates list of users with given input array
@ -42,7 +44,8 @@
:form-params {}
:body-param body
:content-types []
:accepts ["application/json" "application/xml"]})))
:accepts ["application/json" "application/xml"]
:auth-names []})))
(defn login-user
"Logs user into the system
@ -55,7 +58,8 @@
:query-params {"username" username "password" password }
:form-params {}
:content-types []
:accepts ["application/json" "application/xml"]})))
:accepts ["application/json" "application/xml"]
:auth-names []})))
(defn logout-user
"Logs out current logged in user session
@ -67,7 +71,8 @@
:query-params {}
:form-params {}
:content-types []
:accepts ["application/json" "application/xml"]}))
:accepts ["application/json" "application/xml"]
:auth-names []}))
(defn get-user-by-name
"Get user by user name
@ -79,7 +84,8 @@
:query-params {}
:form-params {}
:content-types []
:accepts ["application/json" "application/xml"]}))
:accepts ["application/json" "application/xml"]
:auth-names []}))
(defn update-user
"Updated user
@ -93,7 +99,8 @@
:form-params {}
:body-param body
:content-types []
:accepts ["application/json" "application/xml"]})))
:accepts ["application/json" "application/xml"]
:auth-names []})))
(defn delete-user
"Delete user
@ -105,4 +112,5 @@
:query-params {}
:form-params {}
:content-types []
:accepts ["application/json" "application/xml"]}))
:accepts ["application/json" "application/xml"]
:auth-names []}))

View File

@ -7,12 +7,18 @@
(java.util Date TimeZone)
(java.text SimpleDateFormat)))
(def auth-definitions
{"petstore_auth" {:type :oauth2}
"api_key" {:type :api-key :in :header :param-name "api_key"}})
(def default-api-context
"Default API context."
{:base-url "http://petstore.swagger.io/v2"
:date-format "yyyy-MM-dd"
:datetime-format "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"
:debug false})
:debug false
:auths {"petstore_auth" nil
"api_key" nil}})
(def ^:dynamic *api-context*
"Dynamic API context to be applied in API calls."
@ -20,12 +26,16 @@
(defmacro with-api-context
"A helper macro to wrap *api-context* with default values."
[context & body]
`(binding [*api-context* (merge *api-context* ~context)]
~@body))
[api-context & body]
`(let [api-context# ~api-context
api-context# (-> *api-context*
(merge api-context#)
(assoc :auths (merge (:auths *api-context*) (:auths api-context#))))]
(binding [*api-context* api-context#]
~@body)))
(defmacro check-required-params
"Throw exception if the given parameter value is nil."
"Throw exception if any of the given parameters is nil."
[& params]
(->> params
(map (fn [p]
@ -33,9 +43,9 @@
(throw (IllegalArgumentException. ~(str "The parameter \"" p "\" is required"))))))
(list* 'do)))
(defn- make-date-format
([format-str] (make-date-format format-str nil))
([format-str time-zone]
(defn- ^SimpleDateFormat make-date-format
([^String format-str] (make-date-format format-str nil))
([^String format-str ^String time-zone]
(let [date-format (SimpleDateFormat. format-str)]
(when time-zone
(.setTimeZone date-format (TimeZone/getTimeZone time-zone)))
@ -75,18 +85,45 @@
(-> (make-date-format datetime-format time-zone)
(.parse s)))))
(defn param-to-str [param]
(defn param->str
"Format the given parameter value to string."
[param]
(cond
(instance? Date param) (format-datetime param)
(sequential? param) (str/join "," param)
:else (str param)))
(defn auth->opts
"Process the given auth to an option map that might conatin request options and parameters."
[{:keys [type in param-name]} value]
(case type
:basic {:req-opts {:basic-auth value}}
:oauth2 {:req-opts {:oauth-token value}}
:api-key (case in
:header {:header-params {param-name value}}
:query {:query-params {param-name value}}
(throw (IllegalArgumentException. (str "Invalid `in` for api-key auth: " in))))
(throw (IllegalArgumentException. (str "Invalid auth `type`: " type)))))
(defn process-auth
"Process the given auth name into options, which is merged into the given opts."
[opts auth-name]
(if-let [value (get-in *api-context* [:auths auth-name])]
(merge-with merge
opts
(auth->opts (get auth-definitions auth-name) value))
opts))
(defn auths->opts
"Process the given auth names to an option map that might conatin request options and parameters."
[auth-names]
(reduce process-auth {} auth-names))
(defn make-url
"Make full URL by adding base URL and filling path parameters."
[path path-params]
(let [path (reduce (fn [p [k v]]
(str/replace p (re-pattern (str "\\{" k "\\}")) (param-to-str v)))
(str/replace p (re-pattern (str "\\{" k "\\}")) (param->str v)))
path
path-params)]
(str (:base-url *api-context*) path)))
@ -95,15 +132,15 @@
"Normalize parameter value, handling three cases:
for sequential value, normalize each elements of it;
for File value, do nothing with it;
otherwise, call `param-to-string`."
otherwise, call `param->str`."
[param]
(cond
(sequential? param) (map normalize-param param)
(instance? File param) param
:else (param-to-str param)))
:else (param->str param)))
(defn normalize-params
"Normalize parameters values: remove nils, format to string with `param-to-str`."
"Normalize parameters values: remove nils, format to string with `param->str`."
[params]
(->> params
(remove (comp nil? second))
@ -144,7 +181,7 @@
;; for non-JSON response, return the body string directly
:else body))
(defn form-params-to-multipart
(defn form-params->multipart
"Convert the given form parameters map into a vector as clj-http's :multipart option."
[form-params]
(->> form-params
@ -153,25 +190,27 @@
(defn call-api
"Call an API by making HTTP request and return its response."
[path method {:keys [path-params query-params header-params form-params body-param content-types accepts]}]
[path method {:keys [path-params body-param content-types accepts auth-names] :as opts}]
(let [{:keys [debug]} *api-context*
{:keys [req-opts query-params header-params form-params]} (auths->opts auth-names)
query-params (merge query-params (:query-params opts))
header-params (merge header-params (:header-params opts))
form-params (merge form-params (:form-params opts))
url (make-url path path-params)
content-type (or (json-preferred-mime content-types)
(and body-param :json))
content-type (or (json-preferred-mime content-types) (and body-param :json))
accept (or (json-preferred-mime accepts) :json)
multipart? (= "multipart/form-data" content-type)
opts (cond-> {:url url :method method}
accept (assoc :accept accept)
(seq query-params) (assoc :query-params (normalize-params query-params))
(seq header-params) (assoc :header-params (normalize-params header-params))
(and content-type (not multipart?)) (assoc :content-type content-type)
multipart? (assoc :multipart (-> form-params
normalize-params
form-params-to-multipart))
(and (not multipart?) (seq form-params)) (assoc :form-params (normalize-params form-params))
body-param (assoc :body (serialize body-param content-type))
debug (assoc :debug true :debug-body true))
resp (client/request opts)]
req-opts (cond-> req-opts
true (assoc :url url :method method)
accept (assoc :accept accept)
(seq query-params) (assoc :query-params (normalize-params query-params))
(seq header-params) (assoc :headers (normalize-params header-params))
(and content-type (not multipart?)) (assoc :content-type content-type)
multipart? (assoc :multipart (-> form-params normalize-params form-params->multipart))
(and (not multipart?) (seq form-params)) (assoc :form-params (normalize-params form-params))
body-param (assoc :body (serialize body-param content-type))
debug (assoc :debug true :debug-body true))
resp (client/request req-opts)]
(when debug
(println "Response:")
(println resp))

View File

@ -1,8 +1,15 @@
(ns swagger-petstore.api.pet-test
(:require [clojure.test :refer :all]
[clojure.java.io :as io]
[swagger-petstore.core :refer [with-api-context]]
[swagger-petstore.api.pet :refer :all]))
(defn credentials-fixture [f]
(with-api-context {:auths {"api_key" "special-key"}}
(f)))
(use-fixtures :once credentials-fixture)
(defn- make-random-pet
([] (make-random-pet nil))
([{:keys [id] :as attrs :or {id (System/currentTimeMillis)}}]

View File

@ -1,8 +1,15 @@
(ns swagger-petstore.api.store-test
(:require [clojure.test :refer :all]
[swagger-petstore.core :refer [with-api-context]]
[swagger-petstore.api.store :refer :all])
(:import (java.util Date)))
(defn credentials-fixture [f]
(with-api-context {:auths {"api_key" "special-key"}}
(f)))
(use-fixtures :once credentials-fixture)
(defn- make-random-order []
{:id (+ 90000 (rand-int 10000))
:petId 200

View File

@ -1,7 +1,14 @@
(ns swagger-petstore.api.user-test
(:require [clojure.test :refer :all]
[swagger-petstore.core :refer [with-api-context]]
[swagger-petstore.api.user :refer :all]))
(defn credentials-fixture [f]
(with-api-context {:auths {"api_key" "special-key"}}
(f)))
(use-fixtures :once credentials-fixture)
(defn- make-random-user
([] (make-random-user nil))
([{:keys [id] :as attrs :or {id (System/currentTimeMillis)}}]

View File

@ -9,30 +9,43 @@
(is (= {:base-url "http://petstore.swagger.io/v2"
:date-format "yyyy-MM-dd"
:datetime-format "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"
:debug false}
:debug false
:auths {"api_key" nil
"petstore_auth" nil}}
default-api-context
*api-context*
(with-api-context {}
*api-context*))))
(testing "customize via with-api-context"
(with-api-context {:base-url "http://localhost" :debug true}
(with-api-context {:base-url "http://localhost"
:debug true
:auths {"api_key" "key1"
"petstore_auth" "token1"}}
(is (= {:base-url "http://localhost"
:date-format "yyyy-MM-dd"
:datetime-format "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"
:debug true}
:debug true
:auths {"api_key" "key1"
"petstore_auth" "token1"}}
*api-context*))
;; nested with-api-context inherits values from the outer api context
(with-api-context {:datetime-format "yyyy-MM-dd HH:mm:ss"}
(with-api-context {:datetime-format "yyyy-MM-dd HH:mm:ss"
:auths {"api_key" "key2"}}
(is (= {:base-url "http://localhost"
:date-format "yyyy-MM-dd"
:datetime-format "yyyy-MM-dd HH:mm:ss"
:debug true}
:debug true
:auths {"api_key" "key2"
"petstore_auth" "token1"}}
*api-context*))))
;; back to default api context
(is (= {:base-url "http://petstore.swagger.io/v2"
:date-format "yyyy-MM-dd"
:datetime-format "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"
:debug false}
:debug false
:auths {"api_key" nil
"petstore_auth" nil}}
default-api-context
*api-context*))))
(deftest test-check-required-params
@ -69,10 +82,10 @@
"2015-11-07T00:49:09-03:00")
(is (thrown? ParseException (parse-datetime "2015-11-07T03:49:09.123Z"))))))
(deftest test-param-to-str
(deftest test-param->str
(let [date (parse-datetime "2015-11-07T03:49:09.123Z")]
(are [param expected]
(is (= expected (param-to-str param)))
(is (= expected (param->str param)))
nil ""
"abc" "abc"
123 "123"
@ -80,6 +93,25 @@
[12 "34"] "12,34"
date (format-datetime date))))
(deftest test-auths->opts
(testing "auth values not set by default"
(is (= {} (auths->opts ["api_key" "petstore_auth"])))
(is (= {} (auths->opts []))))
(testing "set api_key"
(with-api-context {:auths {"api_key" "my key"}}
(is (= {:header-params {"api_key" "my key"}} (auths->opts ["api_key" "petstore_auth"])))
(is (= {:header-params {"api_key" "my key"}} (auths->opts ["api_key"])))
(is (= {} (auths->opts ["petstore_auth"])))
(is (= {} (auths->opts [])))))
(testing "set both api_key and petstore_auth"
(with-api-context {:auths {"api_key" "my key" "petstore_auth" "my token"}}
(is (= {:req-opts {:oauth-token "my token"}
:header-params {"api_key" "my key"}}
(auths->opts ["api_key" "petstore_auth"])))
(is (= {:req-opts {:oauth-token "my token"}} (auths->opts ["petstore_auth"])))
(is (= {:header-params {"api_key" "my key"}} (auths->opts ["api_key"])))
(is (= {} (auths->opts []))))))
(deftest test-make-url
(are [path path-params url]
(is (= url (make-url path path-params)))