haveapi 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|