-
Notifications
You must be signed in to change notification settings - Fork 146
Expand file tree
/
Copy pathmeta.clj
More file actions
339 lines (289 loc) · 13 KB
/
meta.clj
File metadata and controls
339 lines (289 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
(ns compojure.api.meta
(:require [compojure.api.common :as common :refer [extract-parameters]]
[compojure.api.middleware :as mw]
[compojure.api.routes :as routes]
[plumbing.core :as p]
[plumbing.fnk.impl :as fnk-impl]
[ring.swagger.common :as rsc]
[ring.swagger.json-schema :as js]
[schema.core :as s]
[schema-tools.core :as st]
[compojure.api.coercion :as coercion]
[compojure.api.help :as help]
compojure.core
compojure.api.compojure-compat
[compojure.api.common :as common]))
(def +compojure-api-request+
"lexically bound ring-request for handlers."
'+compojure-api-request+)
;;
;; Schema
;;
(defn strict [schema]
(dissoc schema 'schema.core/Keyword))
(defn fnk-schema [bind]
(->>
(:input-schema
(fnk-impl/letk-input-schema-and-body-form
nil (with-meta bind {:schema s/Any}) [] nil))
reverse
(into {})))
(s/defn src-coerce!
"Return source code for coerce! for a schema with coercion type,
extracted from a key in a ring request."
([schema, key, type]
(src-coerce! schema, key, type, true))
([schema, key, type, keywordize?]
`(coercion/coerce-request! ~schema ~key ~type ~keywordize? false ~+compojure-api-request+)))
(defn- convert-return [schema]
{200 {:schema schema
:description (or (js/json-schema-meta schema) "")}})
;;
;; Extension point
;;
(defmulti restructure-param
"Restructures a key value pair in smart routes. By default the key
is consumed form the :parameters map in acc. k = given key, v = value."
(fn [k v acc] k))
;;
;; Pass-through swagger metadata
;;
(defmethod restructure-param :summary [k v acc]
(update-in acc [:swagger] assoc k v))
(defmethod restructure-param :description [k v acc]
(update-in acc [:swagger] assoc k v))
(defmethod restructure-param :operationId [k v acc]
(update-in acc [:swagger] assoc k v))
(defmethod restructure-param :consumes [k v acc]
(update-in acc [:swagger] assoc k v))
(defmethod restructure-param :produces [k v acc]
(update-in acc [:swagger] assoc k v))
;;
;; Smart restructurings
;;
; Boolean to discard the route out from api documentation
; Example:
; :no-doc true
(defmethod restructure-param :no-doc [_ v acc]
(update-in acc [:swagger] assoc :x-no-doc v))
; publishes the data as swagger-parameters without any side-effects / coercion.
; Examples:
; :swagger {:responses {200 {:schema User}
; 404 {:schema Error
; :description "Not Found"} }
; :paramerers {:query {:q s/Str}
; :body NewUser}}}
(defmethod restructure-param :swagger [_ swagger acc]
(assoc-in acc [:swagger :swagger] swagger))
; Route name, used with path-for
; Example:
; :name :user-route
(defmethod restructure-param :name [_ v acc]
(update-in acc [:swagger] assoc :x-name v))
; Tags for api categorization. Ignores duplicates.
; Examples:
; :tags [:admin]
(defmethod restructure-param :tags [_ tags acc]
(update-in acc [:swagger :tags] (comp set into) tags))
; Defines a return type and coerces the return value of a body against it.
; Examples:
; :return MySchema
; :return {:value String}
; :return #{{:key (s/maybe Long)}}
(defmethod restructure-param :return [_ schema acc]
(let [response (convert-return schema)]
(-> acc
(update-in [:swagger :responses] (fnil conj []) response)
(update-in [:responses] (fnil conj []) response))))
; value is a map of http-response-code -> Schema. Translates to both swagger
; parameters and return schema coercion. Schemas can be decorated with meta-data.
; Examples:
; :responses {403 nil}
; :responses {403 {:schema ErrorEnvelope}}
; :responses {403 {:schema ErrorEnvelope, :description \"Underflow\"}}
(defmethod restructure-param :responses [_ responses acc]
(-> acc
(update-in [:swagger :responses] (fnil conj []) responses)
(update-in [:responses] (fnil conj []) responses)))
; reads body-params into a enhanced let. First parameter is the let symbol,
; second is the Schema to be coerced! against.
; Examples:
; :body [user User]
(defmethod restructure-param :body [_ [value schema] acc]
(-> acc
(update-in [:lets] into [value (src-coerce! schema :body-params :body)])
(assoc-in [:swagger :parameters :body] schema)))
; reads query-params into a enhanced let. First parameter is the let symbol,
; second is the Schema to be coerced! against.
; Examples:
; :query [user User]
(defmethod restructure-param :query [_ [value schema] acc]
(-> acc
(update-in [:lets] into [value (src-coerce! schema :query-params :string)])
(assoc-in [:swagger :parameters :query] schema)))
; reads header-params into a enhanced let. First parameter is the let symbol,
; second is the Schema to be coerced! against.
; Examples:
; :headers [headers Headers]
(defmethod restructure-param :headers [_ [value schema] acc]
(-> acc
(update-in [:lets] into [value (src-coerce! schema :headers :string)])
(assoc-in [:swagger :parameters :header] schema)))
; restructures body-params with plumbing letk notation. Example:
; :body-params [id :- Long name :- String]
(defmethod restructure-param :body-params [_ body-params acc]
(let [schema (strict (fnk-schema body-params))]
(-> acc
(update-in [:letks] into [body-params (src-coerce! schema :body-params :body)])
(assoc-in [:swagger :parameters :body] schema))))
; restructures form-params with plumbing letk notation. Example:
; :form-params [id :- Long name :- String]
(defmethod restructure-param :form-params [_ form-params acc]
(let [schema (strict (fnk-schema form-params))]
(-> acc
(update-in [:letks] into [form-params (src-coerce! schema :form-params :string)])
(update-in [:swagger :parameters :formData] st/merge schema)
(assoc-in [:swagger :consumes] ["application/x-www-form-urlencoded"]))))
; restructures multipart-params with plumbing letk notation and consumes "multipart/form-data"
; :multipart-params [file :- compojure.api.upload/TempFileUpload]
(defmethod restructure-param :multipart-params [_ params acc]
(let [schema (strict (fnk-schema params))]
(-> acc
(update-in [:letks] into [params (src-coerce! schema :multipart-params :string)])
(update-in [:swagger :parameters :formData] st/merge schema)
(assoc-in [:swagger :consumes] ["multipart/form-data"]))))
; restructures header-params with plumbing letk notation. Example:
; :header-params [id :- Long name :- String]
(defmethod restructure-param :header-params [_ header-params acc]
(let [schema (fnk-schema header-params)]
(-> acc
(update-in [:letks] into [header-params (src-coerce! schema :headers :string)])
(assoc-in [:swagger :parameters :header] schema))))
; restructures query-params with plumbing letk notation. Example:
; :query-params [id :- Long name :- String]
(defmethod restructure-param :query-params [_ query-params acc]
(let [schema (fnk-schema query-params)]
(-> acc
(update-in [:letks] into [query-params (src-coerce! schema :query-params :string)])
(assoc-in [:swagger :parameters :query] schema))))
; restructures path-params by plumbing letk notation. Example:
; :path-params [id :- Long name :- String]
(defmethod restructure-param :path-params [_ path-params acc]
(let [schema (fnk-schema path-params)]
(-> acc
(update-in [:letks] into [path-params (src-coerce! schema :route-params :string)])
(assoc-in [:swagger :parameters :path] schema))))
; Applies the given vector of middlewares to the route
(defmethod restructure-param :middleware [_ middleware acc]
(update-in acc [:middleware] into middleware))
; Bind to stuff in request components using letk syntax
(defmethod restructure-param :components [_ components acc]
(update-in acc [:letks] into [components `(mw/get-components ~+compojure-api-request+)]))
; route-specific override for coercers
(defmethod restructure-param :coercion [_ coercion acc]
(update-in acc [:middleware] conj [mw/wrap-coercion coercion]))
;;
;; Impl
;;
(defmacro dummy-let
"Dummy let-macro used in resolving route-docs. not part of normal invokation chain."
[bindings & body]
(let [bind-form (vec (apply concat (for [n (take-nth 2 bindings)] [n nil])))]
`(let ~bind-form ~@body)))
(defmacro dummy-letk
"Dummy letk-macro used in resolving route-docs. not part of normal invokation chain."
[bindings & body]
(reduce
(fn [cur-body-form [bind-form]]
(if (symbol? bind-form)
`(let [~bind-form nil] ~cur-body-form)
(let [{:keys [map-sym body-form]} (fnk-impl/letk-input-schema-and-body-form
&env
(fnk-impl/ensure-schema-metadata &env bind-form)
[]
cur-body-form)
body-form (clojure.walk/prewalk-replace {'plumbing.fnk.schema/safe-get 'clojure.core/get} body-form)]
`(let [~map-sym nil] ~body-form))))
`(do ~@body)
(reverse (partition 2 bindings))))
(defn routing [handlers]
(if-let [handlers (seq (keep identity (flatten handlers)))]
(apply compojure.core/routes handlers)
(fn ([_] nil) ([_ respond _] (respond nil)))))
;;
;; Api
;;
(defn- destructure-compojure-api-request
"Returns a vector of four elements:
- pruned path string
- new lets list
- bindings form for compojure route
- symbol to which request will be bound"
[path arg]
(let [path-string (if (vector? path) (first path) path)]
(cond
;; GET "/route" []
(vector? arg) [path-string [] (into arg [:as +compojure-api-request+]) +compojure-api-request+]
;; GET "/route" {:as req}
(map? arg) (if-let [as (:as arg)]
[path-string [+compojure-api-request+ as] arg as]
[path-string [] (merge arg [:as +compojure-api-request+]) +compojure-api-request+])
;; GET "/route" req
(symbol? arg) [path-string [+compojure-api-request+ arg] arg arg]
:else (throw
(RuntimeException.
(str "unknown compojure destruction syntax: " arg))))))
(defn merge-parameters
"Merge parameters at runtime to allow usage of runtime-paramers with route-macros."
[{:keys [responses swagger] :as parameters}]
(cond-> parameters
(seq responses) (assoc :responses (common/merge-vector responses))
swagger (-> (dissoc :swagger) (rsc/deep-merge swagger))))
(defn restructure [method [path arg & args] {:keys [context?]}]
(let [[options body] (extract-parameters args true)
[path-string lets arg-with-request arg] (destructure-compojure-api-request path arg)
{:keys [lets
letks
responses
middleware
middlewares
swagger
parameters
body]} (reduce
(fn [acc [k v]]
(restructure-param k v (update-in acc [:parameters] dissoc k)))
{:lets lets
:letks []
:responses nil
:middleware []
:swagger {}
:body body}
options)
;; migration helpers
_ (assert (not middlewares) ":middlewares is deprecated with 1.0.0, use :middleware instead.")
_ (assert (not parameters) ":parameters is deprecated with 1.0.0, use :swagger instead.")
;; response coercion middleware, why not just code?
middleware (if (seq responses) (conj middleware `[coercion/wrap-coerce-response (common/merge-vector ~responses)]) middleware)]
(if context?
;; context
(let [form `(compojure.core/routes ~@body)
form (if (seq letks) `(p/letk ~letks ~form) form)
form (if (seq lets) `(let ~lets ~form) form)
form (if (seq middleware) `((mw/compose-middleware ~middleware) ~form) form)
form `(compojure.core/context ~path ~arg-with-request ~form)
;; create and apply a separate lookup-function to find the inner routes
childs (let [form (vec body)
form (if (seq letks) `(dummy-letk ~letks ~form) form)
form (if (seq lets) `(dummy-let ~lets ~form) form)
form `(compojure.core/let-request [~arg-with-request ~'+compojure-api-request+] ~form)
form `(fn [~'+compojure-api-request+] ~form)
form `(~form {})]
form)]
`(routes/create ~path-string ~method (merge-parameters ~swagger) ~childs ~form))
;; endpoints
(let [form `(do ~@body)
form (if (seq letks) `(p/letk ~letks ~form) form)
form (if (seq lets) `(let ~lets ~form) form)
form (compojure.core/compile-route method path arg-with-request (list form))
form (if (seq middleware) `(compojure.core/wrap-routes ~form (mw/compose-middleware ~middleware)) form)]
`(routes/create ~path-string ~method (merge-parameters ~swagger) nil ~form)))))