haveapi 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/haveapi/action.rb +521 -0
- data/lib/haveapi/actions/default.rb +55 -0
- data/lib/haveapi/actions/paginable.rb +12 -0
- data/lib/haveapi/api.rb +66 -0
- data/lib/haveapi/authentication/base.rb +37 -0
- data/lib/haveapi/authentication/basic/provider.rb +37 -0
- data/lib/haveapi/authentication/chain.rb +110 -0
- data/lib/haveapi/authentication/token/provider.rb +166 -0
- data/lib/haveapi/authentication/token/resources.rb +107 -0
- data/lib/haveapi/authorization.rb +108 -0
- data/lib/haveapi/common.rb +38 -0
- data/lib/haveapi/context.rb +78 -0
- data/lib/haveapi/example.rb +36 -0
- data/lib/haveapi/extensions/action_exceptions.rb +25 -0
- data/lib/haveapi/extensions/base.rb +9 -0
- data/lib/haveapi/extensions/resource_prefetch.rb +7 -0
- data/lib/haveapi/hooks.rb +190 -0
- data/lib/haveapi/metadata.rb +56 -0
- data/lib/haveapi/model_adapter.rb +119 -0
- data/lib/haveapi/model_adapters/active_record.rb +352 -0
- data/lib/haveapi/model_adapters/hash.rb +27 -0
- data/lib/haveapi/output_formatter.rb +57 -0
- data/lib/haveapi/output_formatters/base.rb +29 -0
- data/lib/haveapi/output_formatters/json.rb +9 -0
- data/lib/haveapi/params/param.rb +114 -0
- data/lib/haveapi/params/resource.rb +109 -0
- data/lib/haveapi/params.rb +314 -0
- data/lib/haveapi/public/css/bootstrap-theme.min.css +7 -0
- data/lib/haveapi/public/css/bootstrap.min.css +7 -0
- data/lib/haveapi/public/js/bootstrap.min.js +6 -0
- data/lib/haveapi/public/js/jquery-1.11.1.min.js +4 -0
- data/lib/haveapi/resource.rb +120 -0
- data/lib/haveapi/route.rb +22 -0
- data/lib/haveapi/server.rb +440 -0
- data/lib/haveapi/spec/helpers.rb +103 -0
- data/lib/haveapi/types.rb +24 -0
- data/lib/haveapi/version.rb +3 -0
- data/lib/haveapi/views/doc_layout.erb +27 -0
- data/lib/haveapi/views/doc_sidebars/create-client.erb +20 -0
- data/lib/haveapi/views/doc_sidebars/protocol.erb +42 -0
- data/lib/haveapi/views/index.erb +12 -0
- data/lib/haveapi/views/main_layout.erb +50 -0
- data/lib/haveapi/views/version_page.erb +195 -0
- data/lib/haveapi/views/version_sidebar.erb +42 -0
- data/lib/haveapi.rb +22 -0
- metadata +242 -0
@@ -0,0 +1,352 @@
|
|
1
|
+
module HaveAPI::ModelAdapters
|
2
|
+
# Adapter for ActiveRecord models.
|
3
|
+
class ActiveRecord < ::HaveAPI::ModelAdapter
|
4
|
+
register
|
5
|
+
|
6
|
+
def self.handle?(layout, klass)
|
7
|
+
klass < ::ActiveRecord::Base && %i(object object_list).include?(layout)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.load_validators(model, params)
|
11
|
+
tr = ValidatorTranslator.new(params.params)
|
12
|
+
|
13
|
+
model.validators.each do |validator|
|
14
|
+
tr.translate(validator)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module Action
|
19
|
+
module InstanceMethods
|
20
|
+
# Helper method that sets correct ActiveRecord includes
|
21
|
+
# according to the meta includes sent by the user.
|
22
|
+
# +q+ is the model or partial AR query. If not set,
|
23
|
+
# action's model class is used instead.
|
24
|
+
def with_includes(q = nil)
|
25
|
+
q ||= self.class.model
|
26
|
+
includes = meta && meta[:includes]
|
27
|
+
args = includes.nil? ? [] : ar_parse_includes(includes)
|
28
|
+
|
29
|
+
# Resulting includes may still contain duplicities in form of nested
|
30
|
+
# includes. ar_default_includes returns a flat array where as
|
31
|
+
# ar_parse_includes may contain hashes. But since ActiveRecord is taking
|
32
|
+
# it well, it is not necessary to fix.
|
33
|
+
args.concat(ar_default_includes).uniq
|
34
|
+
|
35
|
+
if !args.empty?
|
36
|
+
q.includes(*args)
|
37
|
+
else
|
38
|
+
q
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Parse includes sent by the user and return them
|
43
|
+
# in an array of symbols and hashes.
|
44
|
+
def ar_parse_includes(raw)
|
45
|
+
return @ar_parsed_includes if @ar_parsed_includes
|
46
|
+
@ar_parsed_includes = ar_inner_includes(raw)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Called by ar_parse_includes for recursion purposes.
|
50
|
+
def ar_inner_includes(includes)
|
51
|
+
args = []
|
52
|
+
|
53
|
+
includes.each do |assoc|
|
54
|
+
if assoc.index('__')
|
55
|
+
tmp = {}
|
56
|
+
parts = assoc.split('__')
|
57
|
+
tmp[parts.first.to_sym] = ar_inner_includes(parts[1..-1])
|
58
|
+
|
59
|
+
args << tmp
|
60
|
+
else
|
61
|
+
args << assoc.to_sym
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
args
|
66
|
+
end
|
67
|
+
|
68
|
+
# Default includes contain all associated resources specified
|
69
|
+
# inaction output parameters. They are fetched from the database
|
70
|
+
# anyway, to return the label for even unresolved association.
|
71
|
+
def ar_default_includes
|
72
|
+
ret = []
|
73
|
+
|
74
|
+
self.class.output.params.each do |p|
|
75
|
+
if p.is_a?(HaveAPI::Parameters::Resource) && self.class.model.reflections[p.name.to_sym]
|
76
|
+
ret << p.name.to_sym
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
ret
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class Input < ::HaveAPI::ModelAdapter::Input
|
86
|
+
def self.clean(model, raw, extra)
|
87
|
+
return if (raw.is_a?(String) && raw.empty?) || (!raw.is_a?(String) && !raw)
|
88
|
+
|
89
|
+
if extra[:fetch]
|
90
|
+
model.instance_exec(raw, &extra[:fetch])
|
91
|
+
else
|
92
|
+
model.find(raw)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class Output < ::HaveAPI::ModelAdapter::Output
|
98
|
+
def self.used_by(action)
|
99
|
+
action.meta(:object) do
|
100
|
+
output do
|
101
|
+
custom :url_params, label: 'URL parameters',
|
102
|
+
desc: 'An array of parameters needed to resolve URL to this object'
|
103
|
+
bool :resolved, label: 'Resolved', desc: 'True if the association is resolved'
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
if %i(object object_list).include?(action.input.layout)
|
108
|
+
clean = Proc.new do |raw|
|
109
|
+
if raw.is_a?(String)
|
110
|
+
raw.strip.split(',')
|
111
|
+
elsif raw.is_a?(Array)
|
112
|
+
raw
|
113
|
+
else
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
desc = <<END
|
119
|
+
A list of names of associated resources separated by a comma.
|
120
|
+
Nested associations are declared with '__' between resource names.
|
121
|
+
For example, 'user,node' will resolve the two associations.
|
122
|
+
To resolve further associations of node, use e.g. 'user,node__location',
|
123
|
+
to go even deeper, use e.g. 'user,node__location__environment'.
|
124
|
+
END
|
125
|
+
|
126
|
+
action.meta(:global) do
|
127
|
+
input do
|
128
|
+
custom :includes, label: 'Included associations',
|
129
|
+
desc: desc, &clean
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
action.send(:include, Action::InstanceMethods)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def has_param?(name)
|
138
|
+
param = @context.action.output[name]
|
139
|
+
param && @object.respond_to?(param.db_name)
|
140
|
+
end
|
141
|
+
|
142
|
+
def [](name)
|
143
|
+
param = @context.action.output[name]
|
144
|
+
v = @object.send(param.db_name)
|
145
|
+
|
146
|
+
if v.is_a?(::ActiveRecord::Base)
|
147
|
+
resourcify(param, v)
|
148
|
+
else
|
149
|
+
v
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def meta
|
154
|
+
params = @context.action.resolve.call(@object)
|
155
|
+
|
156
|
+
{
|
157
|
+
url_params: params.is_a?(Array) ? params : [params],
|
158
|
+
resolved: true
|
159
|
+
}
|
160
|
+
end
|
161
|
+
|
162
|
+
protected
|
163
|
+
# Return representation of an associated resource +param+
|
164
|
+
# with its instance in +val+.
|
165
|
+
#
|
166
|
+
# By default, it returns an unresolved resource, which contains
|
167
|
+
# only object id and label. Resource will be resolved
|
168
|
+
# if it is set in meta includes field.
|
169
|
+
def resourcify(param, val)
|
170
|
+
res_show = param.show_action
|
171
|
+
res_output = res_show.output
|
172
|
+
|
173
|
+
args = res_show.resolve.call(val)
|
174
|
+
|
175
|
+
if includes_include?(param.name)
|
176
|
+
push_cls = @context.action
|
177
|
+
push_ins = @context.action_instance
|
178
|
+
|
179
|
+
pass_includes = includes_pass_on_to(param.name)
|
180
|
+
|
181
|
+
show = res_show.new(
|
182
|
+
push_ins.request,
|
183
|
+
push_ins.version,
|
184
|
+
{},
|
185
|
+
nil,
|
186
|
+
@context
|
187
|
+
)
|
188
|
+
show.meta[:includes] = pass_includes
|
189
|
+
|
190
|
+
# This flag is used to tell the action that it is being used
|
191
|
+
# as a nested association, that it wasn't called directly by the user.
|
192
|
+
show.flags[:inner_assoc] = true
|
193
|
+
|
194
|
+
show.authorized?(push_ins.current_user) # FIXME: handle false
|
195
|
+
|
196
|
+
ret = show.safe_output(val)
|
197
|
+
|
198
|
+
@context.action_instance = push_ins
|
199
|
+
@context.action = push_cls
|
200
|
+
|
201
|
+
fail "#{res_show.to_s} resolve failed" unless ret[0]
|
202
|
+
|
203
|
+
ret[1][res_show.output.namespace].update({
|
204
|
+
_meta: ret[1][:_meta].update(resolved: true)
|
205
|
+
})
|
206
|
+
|
207
|
+
else
|
208
|
+
{
|
209
|
+
param.value_id => val.send(res_output[param.value_id].db_name),
|
210
|
+
param.value_label => val.send(res_output[param.value_label].db_name),
|
211
|
+
_meta: {
|
212
|
+
:url_params => args.is_a?(Array) ? args : [args],
|
213
|
+
:resolved => false
|
214
|
+
}
|
215
|
+
}
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Should an association with +name+ be resolved?
|
220
|
+
def includes_include?(name)
|
221
|
+
includes = @context.action_instance.meta[:includes]
|
222
|
+
return unless includes
|
223
|
+
|
224
|
+
name = name.to_sym
|
225
|
+
|
226
|
+
if @context.action_instance.flags[:inner_assoc]
|
227
|
+
# This action is called as an association of parent resource.
|
228
|
+
# Meta includes are already parsed and can be accessed directly.
|
229
|
+
includes.each do |v|
|
230
|
+
if v.is_a?(::Hash)
|
231
|
+
return true if v.has_key?(name)
|
232
|
+
else
|
233
|
+
return true if v == name
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
false
|
238
|
+
|
239
|
+
else
|
240
|
+
# This action is the one that was called by the user.
|
241
|
+
# Meta includes contains an array of strings as was sent
|
242
|
+
# by the user. The parsed includes must be fetched from
|
243
|
+
# the action itself.
|
244
|
+
includes = @context.action_instance.ar_parse_includes([])
|
245
|
+
|
246
|
+
includes.each do |v|
|
247
|
+
if v.is_a?(::Hash)
|
248
|
+
return true if v.has_key?(name)
|
249
|
+
else
|
250
|
+
return true if v == name
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
false
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# Create an array of includes that is passed to child association.
|
259
|
+
def includes_pass_on_to(assoc)
|
260
|
+
parsed = if @context.action_instance.flags[:inner_assoc]
|
261
|
+
@context.action_instance.meta[:includes]
|
262
|
+
else
|
263
|
+
@context.action_instance.ar_parse_includes([])
|
264
|
+
end
|
265
|
+
|
266
|
+
ret = []
|
267
|
+
|
268
|
+
parsed.each do |v|
|
269
|
+
if v.is_a?(::Hash)
|
270
|
+
v.each { |k, v| ret << v if k == assoc }
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
ret.flatten(1)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
class ValidatorTranslator
|
279
|
+
class << self
|
280
|
+
attr_reader :handlers
|
281
|
+
|
282
|
+
def handle(validator, &block)
|
283
|
+
@handlers ||= {}
|
284
|
+
@handlers[validator] = block
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
handle ::ActiveRecord::Validations::PresenceValidator do |v|
|
289
|
+
validator({present: true})
|
290
|
+
end
|
291
|
+
|
292
|
+
handle ::ActiveModel::Validations::AbsenceValidator do |v|
|
293
|
+
validator({absent: true})
|
294
|
+
end
|
295
|
+
|
296
|
+
handle ::ActiveModel::Validations::ExclusionValidator do |v|
|
297
|
+
validator(v.options)
|
298
|
+
end
|
299
|
+
|
300
|
+
handle ::ActiveModel::Validations::FormatValidator do |v|
|
301
|
+
validator({format: {with_source: v.options[:with].source}.update(v.options)})
|
302
|
+
end
|
303
|
+
|
304
|
+
handle ::ActiveModel::Validations::InclusionValidator do |v|
|
305
|
+
validator(v.options)
|
306
|
+
end
|
307
|
+
|
308
|
+
handle ::ActiveModel::Validations::LengthValidator do |v|
|
309
|
+
validator(v.options)
|
310
|
+
end
|
311
|
+
|
312
|
+
handle ::ActiveModel::Validations::NumericalityValidator do |v|
|
313
|
+
validator(v.options)
|
314
|
+
end
|
315
|
+
|
316
|
+
handle ::ActiveRecord::Validations::UniquenessValidator do |v|
|
317
|
+
validator(v.options)
|
318
|
+
end
|
319
|
+
|
320
|
+
def initialize(params)
|
321
|
+
@params = params
|
322
|
+
end
|
323
|
+
|
324
|
+
def validator_for(param, v)
|
325
|
+
@params.each do |p|
|
326
|
+
next unless p.is_a?(::HaveAPI::Parameters::Param)
|
327
|
+
|
328
|
+
if p.db_name == param
|
329
|
+
p.add_validator(v)
|
330
|
+
break
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
def validator(v)
|
336
|
+
validator_for(@attr, v)
|
337
|
+
end
|
338
|
+
|
339
|
+
def translate(v)
|
340
|
+
self.class.handlers.each do |klass, translator|
|
341
|
+
if v.is_a?(klass)
|
342
|
+
v.attributes.each do |attr|
|
343
|
+
@attr = attr
|
344
|
+
instance_exec(v, &translator)
|
345
|
+
end
|
346
|
+
break
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module HaveAPI::ModelAdapters
|
2
|
+
# Simple hash adapter. Model is just a hash of parameters
|
3
|
+
# and their values.
|
4
|
+
class Hash < ::HaveAPI::ModelAdapter
|
5
|
+
register
|
6
|
+
|
7
|
+
def self.handle?(layout, klass)
|
8
|
+
klass.is_a?(::Hash)
|
9
|
+
end
|
10
|
+
|
11
|
+
class Input < ::HaveAPI::ModelAdapter::Input
|
12
|
+
def self.clean(model, raw)
|
13
|
+
raw
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Output < ::HaveAPI::ModelAdapter::Output
|
18
|
+
def has_param?(name)
|
19
|
+
@object.has_key?(name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def [](name)
|
23
|
+
@object[name]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module HaveAPI
|
2
|
+
module OutputFormatters
|
3
|
+
|
4
|
+
end
|
5
|
+
|
6
|
+
class OutputFormatter
|
7
|
+
class << self
|
8
|
+
attr_reader :formatters
|
9
|
+
|
10
|
+
def register(klass)
|
11
|
+
@formatters ||= []
|
12
|
+
@formatters << klass
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def supports?(types)
|
17
|
+
@formatter = nil
|
18
|
+
|
19
|
+
if types.empty?
|
20
|
+
return @formatter = self.class.formatters.first.new
|
21
|
+
end
|
22
|
+
|
23
|
+
types.each do |type|
|
24
|
+
self.class.formatters.each do |f|
|
25
|
+
if f.handle?(type)
|
26
|
+
@formatter = f.new
|
27
|
+
break
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
@formatter.nil? ? false : true
|
33
|
+
end
|
34
|
+
|
35
|
+
def format(status, response, message = nil, errors = nil)
|
36
|
+
@formatter.format(header(status, response, message, errors))
|
37
|
+
end
|
38
|
+
|
39
|
+
def error(msg)
|
40
|
+
@formatter.format(header(false, nil, msg))
|
41
|
+
end
|
42
|
+
|
43
|
+
def content_type
|
44
|
+
@formatter.content_type
|
45
|
+
end
|
46
|
+
|
47
|
+
protected
|
48
|
+
def header(status, response, message = nil, errors = nil)
|
49
|
+
{
|
50
|
+
status: status,
|
51
|
+
response: response,
|
52
|
+
message: message,
|
53
|
+
errors: errors
|
54
|
+
}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module HaveAPI::OutputFormatters
|
2
|
+
class BaseFormatter
|
3
|
+
class << self
|
4
|
+
attr_reader :types
|
5
|
+
|
6
|
+
def handle(*args)
|
7
|
+
@types ||= []
|
8
|
+
@types += args
|
9
|
+
|
10
|
+
HaveAPI::OutputFormatter.register(Kernel.const_get(self.to_s)) unless @registered
|
11
|
+
@registered = true
|
12
|
+
end
|
13
|
+
|
14
|
+
def handle?(type)
|
15
|
+
@types.detect do |t|
|
16
|
+
File.fnmatch(type, t)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def content_type
|
22
|
+
self.class.types.first
|
23
|
+
end
|
24
|
+
|
25
|
+
def format(response)
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module HaveAPI::Parameters
|
2
|
+
class Param
|
3
|
+
attr_reader :name, :label, :desc, :type, :default
|
4
|
+
|
5
|
+
def initialize(name, required: nil, label: nil, desc: nil, type: nil,
|
6
|
+
choices: nil, db_name: nil, default: :_nil, fill: false,
|
7
|
+
clean: nil)
|
8
|
+
@required = required
|
9
|
+
@name = name
|
10
|
+
@label = label || name.to_s.capitalize
|
11
|
+
@desc = desc
|
12
|
+
@type = type
|
13
|
+
@choices = choices
|
14
|
+
@db_name = db_name
|
15
|
+
@default = default
|
16
|
+
@fill = fill
|
17
|
+
@layout = :custom
|
18
|
+
@validators = {}
|
19
|
+
@clean = clean
|
20
|
+
end
|
21
|
+
|
22
|
+
def db_name
|
23
|
+
@db_name || @name
|
24
|
+
end
|
25
|
+
|
26
|
+
def required?
|
27
|
+
@required
|
28
|
+
end
|
29
|
+
|
30
|
+
def optional?
|
31
|
+
!@required
|
32
|
+
end
|
33
|
+
|
34
|
+
def fill?
|
35
|
+
@fill
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_validator(v)
|
39
|
+
@validators.update(v)
|
40
|
+
end
|
41
|
+
|
42
|
+
def validators
|
43
|
+
@validators
|
44
|
+
end
|
45
|
+
|
46
|
+
def describe(context)
|
47
|
+
{
|
48
|
+
required: required?,
|
49
|
+
label: @label,
|
50
|
+
description: @desc,
|
51
|
+
type: @type ? @type.to_s : String.to_s,
|
52
|
+
choices: @choices,
|
53
|
+
validators: @validators,
|
54
|
+
default: @default
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def patch(attrs)
|
59
|
+
attrs.each { |k, v| instance_variable_set("@#{k}", v) }
|
60
|
+
end
|
61
|
+
|
62
|
+
def clean(raw)
|
63
|
+
return instance_exec(raw, &@clean) if @clean
|
64
|
+
|
65
|
+
val = if raw.nil?
|
66
|
+
@default
|
67
|
+
|
68
|
+
elsif @type.nil?
|
69
|
+
nil
|
70
|
+
|
71
|
+
elsif @type == Integer
|
72
|
+
raw.to_i
|
73
|
+
|
74
|
+
elsif @type == Boolean
|
75
|
+
Boolean.to_b(raw)
|
76
|
+
|
77
|
+
elsif @type == ::Datetime
|
78
|
+
begin
|
79
|
+
Time.iso8601(raw)
|
80
|
+
|
81
|
+
rescue ArgumentError
|
82
|
+
raise HaveAPI::ValidationError.new("not in ISO 8601 format '#{raw}'")
|
83
|
+
end
|
84
|
+
|
85
|
+
else
|
86
|
+
raw
|
87
|
+
end
|
88
|
+
|
89
|
+
if @choices
|
90
|
+
if @choices.is_a?(Array)
|
91
|
+
unless @choices.include?(val) || @choices.include?(val.to_s.to_sym)
|
92
|
+
raise HaveAPI::ValidationError.new("invalid choice '#{raw}'")
|
93
|
+
end
|
94
|
+
|
95
|
+
elsif @choices.is_a?(Hash)
|
96
|
+
unless @choices.has_key?(val) || @choices.has_key?(val.to_s.to_sym)
|
97
|
+
raise HaveAPI::ValidationError.new("invalid choice '#{raw}'")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
val
|
103
|
+
end
|
104
|
+
|
105
|
+
def format_output(v)
|
106
|
+
if @type == ::Datetime && v.is_a?(Time)
|
107
|
+
v.iso8601
|
108
|
+
|
109
|
+
else
|
110
|
+
v
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module HaveAPI::Parameters
|
2
|
+
class Resource
|
3
|
+
attr_reader :name, :resource, :label, :desc, :type, :value_id, :value_label,
|
4
|
+
:choices, :value_params
|
5
|
+
|
6
|
+
def initialize(resource, name: nil, label: nil, desc: nil,
|
7
|
+
choices: nil, value_id: :id, value_label: :label, required: nil,
|
8
|
+
db_name: nil, fetch: nil)
|
9
|
+
@resource = resource
|
10
|
+
@resource_path = build_resource_path(resource)
|
11
|
+
@name = name || resource.to_s.demodulize.underscore.to_sym
|
12
|
+
@label = label || (name && name.to_s.capitalize) || resource.to_s.demodulize
|
13
|
+
@desc = desc
|
14
|
+
@choices = choices || @resource::Index
|
15
|
+
@value_id = value_id
|
16
|
+
@value_label = value_label
|
17
|
+
@required = required
|
18
|
+
@db_name = db_name
|
19
|
+
@extra = {
|
20
|
+
fetch: fetch
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def db_name
|
25
|
+
@db_name || @name
|
26
|
+
end
|
27
|
+
|
28
|
+
def required?
|
29
|
+
@required
|
30
|
+
end
|
31
|
+
|
32
|
+
def optional?
|
33
|
+
!@required
|
34
|
+
end
|
35
|
+
|
36
|
+
def show_action
|
37
|
+
@resource::Show
|
38
|
+
end
|
39
|
+
|
40
|
+
def describe(context)
|
41
|
+
val_url = context.url_for(
|
42
|
+
@resource::Show,
|
43
|
+
context.endpoint && context.action_prepare && context.layout == :object && context.call_url_params(context.action, context.action_prepare)
|
44
|
+
)
|
45
|
+
val_method = @resource::Index.http_method.to_s.upcase
|
46
|
+
|
47
|
+
choices_url = context.url_for(
|
48
|
+
@choices,
|
49
|
+
context.endpoint && context.layout == :object && context.call_url_params(context.action, context.action_prepare)
|
50
|
+
)
|
51
|
+
choices_method = @choices.http_method.to_s.upcase
|
52
|
+
|
53
|
+
{
|
54
|
+
required: required?,
|
55
|
+
label: @label,
|
56
|
+
description: @desc,
|
57
|
+
type: 'Resource',
|
58
|
+
resource: @resource_path,
|
59
|
+
value_id: @value_id,
|
60
|
+
value_label: @value_label,
|
61
|
+
value: context.action_prepare && {
|
62
|
+
url: val_url,
|
63
|
+
method: val_method,
|
64
|
+
help: "#{val_url}?method=#{val_method}",
|
65
|
+
},
|
66
|
+
choices: {
|
67
|
+
url: choices_url,
|
68
|
+
method: choices_method,
|
69
|
+
help: "#{choices_url}?method=#{choices_method}"
|
70
|
+
}
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
def patch(attrs)
|
75
|
+
attrs.each { |k, v| instance_variable_set("@#{k}", v) }
|
76
|
+
end
|
77
|
+
|
78
|
+
def clean(raw)
|
79
|
+
::HaveAPI::ModelAdapter.for(
|
80
|
+
show_action.input.layout, @resource.model
|
81
|
+
).input_clean(@resource.model, raw, @extra)
|
82
|
+
end
|
83
|
+
|
84
|
+
def format_output(v)
|
85
|
+
v
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
def build_resource_path(r)
|
90
|
+
path = []
|
91
|
+
top_module = Kernel
|
92
|
+
|
93
|
+
r.to_s.split('::').each do |name|
|
94
|
+
top_module = top_module.const_get(name)
|
95
|
+
|
96
|
+
begin
|
97
|
+
top_module.obj_type
|
98
|
+
|
99
|
+
rescue NoMethodError
|
100
|
+
next
|
101
|
+
end
|
102
|
+
|
103
|
+
path << name.underscore
|
104
|
+
end
|
105
|
+
|
106
|
+
path
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|