/
actions.clj
197 lines (158 loc) · 7.45 KB
/
actions.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
;; Copyright (c) Tomek Lipski. All rights reserved. The use
;; and distribution terms for this software are covered by the Eclipse
;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
;; which can be found in the file LICENSE.txt at the root of this
;; distribution. By using this software in any fashion, you are
;; agreeing to be bound by the terms of this license. You must not
;; remove this notice, or any other, from this software.
(ns ganelon.web.actions
"This namespace contains actions support functions and macros."
(:require [ganelon.util.logging :as logging]
[compojure.core :as compojure]
[noir.response :as response]
[noir.request :as request]
[hiccup.core :as hiccup]
[ganelon.util :as common]
[ganelon.web.dyna-routes :as dr]
[ganelon.web.ui-operations :as ui]
))
(def ^:dynamic *widget-id*
"Id of a current widget, for example provided as a 'widget-id' GET/POST parameter."
nil)
(def ^:dynamic *operation-queue* (atom []))
(defonce ACTION-REGISTRY (atom {}))
(defmacro ACTION
"Define and register basic action, by adding: logging and exception handling to compojure.core/ANY handler with path
composed from '/a/' prefix and action name.
Example:
(ACTION \"sample4\" [x y]
(noir.response/json {:x x :y y :z \"!\"}))
This macro does not register handler with ganelon.web.dyna-routes."
[name params & body]
`(do
(swap! ACTION-REGISTRY #(assoc % ~name ~(into [] (map str params))))
(compojure/ANY ~(str "/a/" name) ~params
(try
(logging/log :debug " invoke " ~(str name))
(logging/log-val :trace " result " ~(str name) (do ~@body))
(catch Throwable t#
(logging/handle-exception (str "error while invoking action " ~(str name)) t#)
(response/json
[{:type "error"
:message (hiccup/html
[:p "An error has occured during the processing of your request. We have logged what has happened, and will look into
the problem with details. If the problem persists and causes any trouble to you, you can also <a href=\"/contact\">contact us</a>."]
[:p "The error message was " [:b (.getMessage t#)]])
}]))))))
(defn wrap-set-widget-id
"Bind 'widget-id' req parameter to a special variable *widget-id*.
This middleware requires wrap-keyword-params."
[app]
(fn [req]
(binding [*widget-id* (:widget-id (:params req))]
(app req))))
(defn put-operation!
"Register operation to be returned as a part of the action response. Returns nil in case the put-operation!
is used at the end of an action body.
Example:
(actions/put-operation! (ui/notification \"Success\"
(h/html \"Message set to: \" [:b (hiccup.util/escape-html msg)])))"
[& operations]
(swap! *operation-queue* #(apply conj % operations))
nil)
(defmacro JSONACTION
"Define and register JSON action, by adding: logging and exception handling to compojure.core/ANY handler with path
composed from '/a/' prefix and action name.
If the body result is a not collection or is a map, it is wrapped in a vector. The final result is passed to
noir.response/json function.
Example:
(JSONACTION \"sample4\" [x y]
{:x x :y y :z \"!\"})
This macro does not register handler with ganelon.web.dyna-routes."
[name params & body]
`(ACTION ~name ~params
(binding [*operation-queue* (atom [])]
(let [res# (do ~@body)]
(if (empty? @*operation-queue*)
(response/json (if (and (coll? res#) (not (map? res#))) res# [res#]))
(response/json (if res#
(flatten [@*operation-queue* (if (and (coll? res#) (not (map? res#))) res# [res#])])
@*operation-queue*)))))))
(defmacro WIDGETACTION
"Define and register widget action, by adding: logging and exception handling to compojure.core/ANY handler with path
composed from '/a/' prefix and action name.
The result of body is used as a value for ganelon.web.ui-operations/replace-with. The id is taken from *widget-id*
special variable. The operation is wrapped with JSONACTION macro.
If ~body returns nil, the widget is not updated.
Example:
(WIDGETACTION \"sample4\" [x y]
(str \"<p>Test!\" (int x) \"</p>\"))
This macro does not register handler with ganelon.web.dyna-routes."
[name params & body]
`(->
(JSONACTION ~name ~params
(when-let [res# (do ~@body)]
(ui/replace-with
(or (:widget-id (:params request/*request*)) (get (:params request/*request*) "widget-id"))
res#)))
wrap-set-widget-id))
(defmacro defaction
"This macro wraps ACTION with ganelon.web.dyna-routes/setroute! function, registering action for a default
dyna-routes handler.
Example:
(defaction \"sample4\" [x y]
(noir.response/json {:x x :y y :z \"!\"}))"
[name params & body]
`(dr/setroute! ~name
(ACTION ~name ~params ~@body)))
(defmacro defjsonaction
"This macro wraps JSONACTION with ganelon.web.dyna-routes/setroute! function, registering action for a default
dyna-routes handler.
Example:
(defjsonaction \"sample4\" [x y]
{:x x :y y :z \"!\"})"
[name params & body]
`(dr/setroute! ~name
(JSONACTION ~name ~params ~@body)))
(defmacro defwidgetaction
"This macro wraps JSONACTION with ganelon.web.dyna-routes/setroute! function, registering action for a default
dyna-routes handler.
Example:
(defwidgetaction \"sample4\" [x y]
(str \"<p>Test!\" (int x) \"</p>\"))"
[name params & body]
`(dr/setroute! ~name
(WIDGETACTION ~name ~params ~@body)))
(defn- javascriptize [v]
(common/mreplace v
[["-" "_"]
["/" "_"]]
))
(defn javascript-action-interface
"Generate action interface for JavaScript. Dash (-) and slash (/) characters are translated to an underscore (_).
If the action does not use widget-id parameter, it will be added implicitly at the end of params.
Example:
(javascript-action-interface ['test-action ['widget-id 'x 'y]])
;=> \"GanelonAction.test_action=function(widget_id,x,y, onSuccess, onError){Ganelon.performAction('test-action','widget-id='+encodeURIComponent(widget_id)+'&x='+encodeURIComponent(x)+'&y='+encodeURIComponent(y)+'',onSuccess,onError);};\"
(javascript-action-interface ['test-action ['x 'y 'z]])
;=> \"GanelonAction.test_action=function(x,y,z,widget_id, onSuccess, onError){Ganelon.performAction('test-action','x='+encodeURIComponent(x)+'&y='+encodeURIComponent(y)+'&z='+encodeURIComponent(z)+'&widget-id='+encodeURIComponent(widget_id)+'',onSuccess,onError);};\""
[[name params]]
(let [params (if (not (some #{"widget-id"} (map str params))) (conj params "widget-id") params)]
(str
"GanelonAction." (javascriptize name) "=function("
(apply str (interpose "," (map javascriptize params))) ", onSuccess, onError){"
"Ganelon.performAction('" name "','"
(apply str (interpose "&" (map #(str %1 "='+encodeURIComponent(" (javascriptize %1) ")+'") params)))
"',onSuccess,onError);};")))
(defn javascript-actions-handler
"Ring handler that returns a JavaScript file containing action interfaces for all registered actions.
Usually, it is more convienient to use ganelon.web.app/javascript-actions-handler, which provides a route
to this handler for GET on /ganelon/actions.js.
Example use:
(compojure/GET \"/ganelon/actions.js\" []
(actions/javascript-actions-handler))"
[]
(response/content-type "text/javascript"
(apply str
"var GanelonAction=GanelonAction||{};"
(map javascript-action-interface @ACTION-REGISTRY))))