-
Notifications
You must be signed in to change notification settings - Fork 6
/
handler.rb
231 lines (200 loc) · 8.31 KB
/
handler.rb
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
module Lev
class Paramifier
include ActiveAttr::Model
include ActiveAttr::Typecasting
# Hack to provide ActiveAttr's Boolean type concisely
def self.boolean; ActiveAttr::Typecasting::Boolean; end
def as_hash(*keys)
keys = [keys].flatten.compact
Hash[keys.collect { |key| [key, self.send(key)] }]
end
end
# Common methods for all handlers. Handlers are extensions of Routines
# and are responsible for taking input data from a form or other widget and
# doing something with it. See Lev::Routine for more information.
#
# All handlers must:
# 2) call "lev_handler"
# 3) implement the 'handle' method which takes no arguments and does the
# work the handler is charged with
# 4) implement the 'authorized?' method which returns true iff the
# caller is authorized to do what the handler is charged with
#
# Handlers may:
# 1) implement the 'setup' method which runs before 'authorized?' and 'handle'.
# This method can do anything, and will likely include setting up some
# instance objects based on the params.
# 2) Call the class method "paramify" to declare, cast, and validate parts of
# the params hash. The first argument to paramify is the key in params
# which points to a hash of params to be paramified. If this first argument
# is unspecified (or specified as `:paramify`, a reserved symbol), the entire
# params hash will be paramified. The block passed to paramify looks just
# like the guts of an ActiveAttr model.
#
# When the incoming params includes :search => {:type, :terms, :num_results}
# the Handler class would look like:
#
# class MyHandler
# lev_handler
#
# paramify :search do
# attribute :search_type, type: String
# validates :search_type, presence: true,
# inclusion: { in: %w(Name Username Any),
# message: "is not valid" }
#
# attribute :search_terms, type: String
# validates :search_terms, presence: true
#
# attribute :num_results, type: Integer
# validates :num_results, numericality: { only_integer: true,
# greater_than_or_equal_to: 0 }
# end
#
# def handle
# # By this time, if there were any errors the handler would have
# # already populated the errors object and returned.
# #
# # Paramify makes a 'search_params' attribute available through
# # which you can access the paramified params, e.g.
# x = search_params.num_results
# ...
# end
# end
#
# Note that if you want to use a "Boolean" type, you need to type it with
# a lowercase (type: boolean).
#
# All handler instance methods have the following available to them:
# 1) 'params' -- the params from the input
# 2) 'caller' -- the user submitting the input
# 3) 'errors' -- an object in which to store errors
# 4) 'results' -- a hash in which to store results for return to calling code
# 5) 'request' -- the HTTP request
# 6) 'options' -- a hash containing the options passed in, useful for other
# nonstandard data.
#
# These methods are available iff these data were supplied in the call
# to the handler (not all handlers need all of this). However, note that
# the Lev::HandleWith module supplies an easy way to call Handlers from
# controllers -- when this way is used, all of the methods above are available.
#
# Handler 'handle' methods don't return anything; they just set values in
# the errors and results objects. The documentation for each handler
# should explain what the results will be and any nonstandard data required
# to be passed in in the options.
#
# In addition to the class- and instance-level "call" methods provided by
# Lev::Routine, Handlers have a class-level "handle" method (an alias of
# the class-level "call" method). The convention for handlers is that the
# call methods take a hash of options/inputs. The instance-level handle
# method doesn't take any arguments since the arguments have been stored
# as instance variables by the time the instance-level handle method is called.
#
# Example:
#
# class MyHandler
# lev_handler
# protected
# def authorized?
# # return true iff exec is allowed to be called, e.g. might
# # check the caller against the params
# def handle
# # do the work, add errors to errors object and results to the results hash as needed
# end
# end
#
module Handler
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
include Lev::Routine
end
end
module ClassMethods
def handle(options={})
call(options)
end
def paramify(group = :paramify, options={}, &block)
method_name = "#{group.to_s}_params"
variable_sym = "@#{method_name}".to_sym
# Generate the dynamic ActiveAttr class given
# the paramify block; I think the caching of the class
# in paramify_classes is only necessary to maintain
# the name of the class set in the const_set statement
if paramify_classes[group].nil?
paramify_classes[group] = Class.new(Lev::Paramifier) do
cattr_accessor :group
end
# Attach a name to this dynamic class
const_set("#{group.to_s.capitalize}Paramifier",
paramify_classes[group])
paramify_classes[group].class_eval(&block)
paramify_classes[group].group = group
end
# Define the "#{group}_params" method to get the paramifier
# instance wrapping the params. Choose the subset of params
# based on the group, choosing all params if the default group
# is used.
define_method method_name.to_sym do
if !instance_variable_get(variable_sym)
params_subset = group == :paramify ? params : params[group]
instance_variable_set(variable_sym,
self.class.paramify_classes[group].new(params_subset))
end
instance_variable_get(variable_sym)
end
# Keep track of the accessor for the params so we can check
# errors in it later
paramify_methods.push(method_name.to_sym)
end
def paramify_methods
@paramify_methods ||= []
end
def paramify_classes
@paramify_classes ||= {}
end
end
protected
attr_accessor :params
attr_accessor :request
attr_accessor :options
attr_accessor :caller
# Provided for development / debugging purposes -- gives a way to pass
# information in a raised security transgression when authorized? is false
attr_accessor :auth_error_details
# This is a method required by Lev::Routine. It enforces the steps common
# to all handlers.
def exec(options)
self.params = options[:params]
self.request = options[:request]
self.caller = options[:caller]
self.options = options.except(:params, :request, :caller)
setup
raise Lev.configuration.security_transgression_error, auth_error_details unless authorized?
validate_paramified_params
handle unless errors?
end
# Default setup implementation -- a no-op
def setup; end
# Default authorized? implementation. It returns true so that every
# handler realization has to make a conscious decision about who is authorized
# to call the handler. To help the common error of forgetting to override this
# method in a handler instance, we provide an error message when this default
# implementation is called.
def authorized?
self.auth_error_details =
"Access to handlers is prevented by default. You need to override the " +
"'authorized?' in this handler to explicitly grant access."
false
end
# Helper method to validate paramified params and to transfer any errors
# into the handler.
def validate_paramified_params
self.class.paramify_methods.each do |method|
params = send(method)
transfer_errors_from(params, TermMapper.scope(params.group)) if !params.valid?
end
end
end
end