/
liberator.clj
211 lines (179 loc) · 6.39 KB
/
liberator.clj
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
(ns sweet-tooth.endpoint.liberator
"Includes:
* Utility functions for retrieving common values from the
context (`record`, `errrors`, `params`)
* A template of decision defaults appropriate for an SPA
* Utility functions for auth
Introduces loose convention of putting a record under `:record` in
the context
Extends liberator's representations to handle transit"
(:require [buddy.auth :as buddy]
[flyingmachine.webutils.validation :refer [if-valid]]
[liberator.representation :as lr]
[medley.core :as medley]
[ring.util.response :as resp]
[sweet-tooth.describe :as d]))
;; -------------------------
;; returning transit
;; -------------------------
(defrecord TransitResponse [data]
liberator.representation.Representation
(as-response [_ context]
{:body data
:headers {"Content-Type" (get-in context [:representation :media-type])}}))
(defmethod lr/render-map-generic "application/transit+json"
[data _ctx]
(->TransitResponse data))
(defmethod lr/render-map-generic "application/transit+msgpack"
[data _ctx]
(->TransitResponse data))
(defmethod lr/render-seq-generic "application/transit+json"
[data _ctx]
(->TransitResponse data))
(defmethod lr/render-seq-generic "application/transit+msgpack"
[data _ctx]
(->TransitResponse data))
(defn transit-response
[payload & [opts]]
(lr/ring-response
payload
(merge {:status (get opts :status 200)
:headers {"media-type" "application/transit+json"}}
opts)))
;; -------------------------
;; Working with liberator context
;; -------------------------
(defn get-ctx
[path]
(let [path (if (vector? path) path [path])]
(fn [ctx]
(get-in ctx path))))
;; Expect consumers to store records when e.g. fetching a single ent
;; by id in record
(def record (get-ctx :record))
(def errors (get-ctx :errors))
(def params (get-ctx [:request :params]))
(defn errors-map
"Add errors to context, setting media-type in case liberator doesn't
get to that decision"
[errors]
{:errors errors
:representation {:media-type "application/transit+json"}})
(defn error-response
"For cases where the error happens before the request gets to liberator"
[status errors]
(transit-response [:errors errors] {:status status}))
(defn ->ctx
"Make it easy to thread data into liberator context"
[x k]
{k x})
(defn exists-fn
"Given a function to retrieve a record, store it under `:record` in the
context if it exists"
[ent-fn]
(fn [ctx]
(if-let [ent (ent-fn ctx)]
{:record ent}
false)))
;; -------------------------
;; decisions
;; -------------------------
;; Generating liberator resources without defresource
;; TODO check if there's something better than handle-malformed
(def decision-defaults
"A base set of liberator resource decisions"
(let [errors-in-ctx (fn [ctx] [:errors (:errors ctx)])
base {:available-media-types ["application/transit+json"
"application/transit+msgpack"
"application/json"]
:allowed-methods [:get]
:authorized? true
:handle-unauthorized errors-in-ctx
:handle-malformed errors-in-ctx
:respond-with-entity? true
:new? false}]
{:get base
:post (merge base {:allowed-methods [:post]
:new? true
:handle-created record})
:put (merge base {:allowed-methods [:put]})
:patch (merge base {:allowed-methods [:patch]})
:head (merge base {:allowed-methods [:head]})
:delete (merge base {:allowed-methods [:delete]
:respond-with-entity? false})}))
(defn initialize-decisions
"Adds `:initalize` to multiple decisions. Used by
`sweet-tooth.endpoint.module.liberator-reitit-router` to inject
context values set in routes."
[decisions context-initializer]
(medley/map-vals
(fn [decision]
(assoc decision :initialize-context (if (fn? context-initializer)
context-initializer
(constantly context-initializer))))
decisions))
;; -------------------------
;; validation
;; -------------------------
(defmacro validator
"Used in invalid? which is why truth values are reversed"
([validation]
`(validator ~(gensym) ~validation))
([ctx-sym validation]
`(fn [~ctx-sym]
(if-valid
(params ~ctx-sym) ~validation errors#
false
[true (errors-map errors#)]))))
(defn validate-describe
"Use describe lib to validate a request. Returns a function that's
meant to be used with the `:malformed?` liberator decision"
[rules & [describe-context]]
(fn [ctx]
(when-let [descriptions (d/describe (params ctx)
rules
(when describe-context (describe-context ctx)))]
[true (errors-map (d/map-rollup-descriptions descriptions))])))
;; -------------------------
;; auth
;; -------------------------
(def authorization-error
(errors-map {:authorization "Not authorized."}))
(def authentication-error
(errors-map {:authentication "You must be logged in to do that."}))
;; Assumes buddy
(def auth (get-ctx [:request :identity]))
(defn authenticated?
"To use with liberator's :authorized? decision"
[ctx]
(if (buddy/authenticated? (:request ctx))
[true {:auth (:identity (:request ctx))}]
[false authentication-error]))
(defn auth-with
"If any auth function authenticates the context, return true. Used
to e.g. auth by ownership or adminship"
[& fns]
(fn [ctx]
(if (some #(% ctx) fns)
true
[false authorization-error])))
(defn auth-id
"Retrieve the ID of authenticated user. Assumes `:auth-id-key` is in
the ctx"
[{:keys [auth-id-key] :as ctx}]
{:pre [auth-id-key]}
((:auth-id-key ctx) (auth ctx)))
(defn assoc-user
[ctx user-key]
(assoc-in ctx
[:request :params user-key]
(auth-id ctx)))
;; -------------------------
;; misc
;; -------------------------
;; TODO move this somwhere else, it's not really liberator
(defn html-resource
"Serve resource at `path` as html"
[path]
(-> (resp/resource-response path)
(resp/content-type "text/html")))