haveapi 0.3.2 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +12 -0
- data/Gemfile +10 -10
- data/README.md +19 -12
- data/Rakefile +23 -0
- data/doc/hooks.erb +35 -0
- data/doc/index.md +1 -0
- data/doc/json-schema.erb +369 -0
- data/doc/protocol.md +178 -38
- data/doc/protocol.plantuml +220 -0
- data/haveapi.gemspec +6 -10
- data/lib/haveapi/action.rb +35 -6
- data/lib/haveapi/api.rb +22 -5
- data/lib/haveapi/common.rb +7 -0
- data/lib/haveapi/exceptions.rb +7 -0
- data/lib/haveapi/hooks.rb +19 -8
- data/lib/haveapi/model_adapters/active_record.rb +58 -19
- data/lib/haveapi/output_formatter.rb +8 -5
- data/lib/haveapi/output_formatters/json.rb +6 -1
- data/lib/haveapi/params/param.rb +33 -39
- data/lib/haveapi/params/resource.rb +20 -0
- data/lib/haveapi/params.rb +26 -4
- data/lib/haveapi/public/doc/protocol.png +0 -0
- data/lib/haveapi/resource.rb +2 -7
- data/lib/haveapi/server.rb +87 -26
- data/lib/haveapi/tasks/hooks.rb +3 -0
- data/lib/haveapi/tasks/yard.rb +12 -0
- data/lib/haveapi/validator.rb +134 -0
- data/lib/haveapi/validator_chain.rb +99 -0
- data/lib/haveapi/validators/acceptance.rb +38 -0
- data/lib/haveapi/validators/confirmation.rb +46 -0
- data/lib/haveapi/validators/custom.rb +21 -0
- data/lib/haveapi/validators/exclusion.rb +38 -0
- data/lib/haveapi/validators/format.rb +42 -0
- data/lib/haveapi/validators/inclusion.rb +42 -0
- data/lib/haveapi/validators/length.rb +71 -0
- data/lib/haveapi/validators/numericality.rb +104 -0
- data/lib/haveapi/validators/presence.rb +40 -0
- data/lib/haveapi/version.rb +2 -1
- data/lib/haveapi/views/doc_sidebars/json-schema.erb +7 -0
- data/lib/haveapi/views/main_layout.erb +11 -0
- data/lib/haveapi/views/version_page.erb +26 -3
- data/lib/haveapi.rb +7 -4
- metadata +45 -66
data/lib/haveapi/api.rb
CHANGED
@@ -33,22 +33,31 @@ module HaveAPI
|
|
33
33
|
# Return list of resources for version +v+.
|
34
34
|
def self.get_version_resources(module_name, v)
|
35
35
|
filter_resources(module_name) do |r|
|
36
|
-
|
36
|
+
r_v = r.version || implicit_version
|
37
|
+
|
38
|
+
if r_v.is_a?(Array)
|
39
|
+
r_v.include?(v)
|
40
|
+
|
41
|
+
else
|
42
|
+
r_v == v || r_v == :all
|
43
|
+
end
|
37
44
|
end
|
38
45
|
end
|
39
46
|
|
40
47
|
# Return a list of all API versions.
|
41
|
-
def self.
|
48
|
+
def self.versions(module_name)
|
42
49
|
ret = []
|
43
50
|
|
44
51
|
resources(module_name) do |r|
|
45
52
|
ret << r.version unless ret.include?(r.version)
|
46
53
|
end
|
47
54
|
|
55
|
+
ret.compact!
|
56
|
+
ret << implicit_version if ret.empty? && implicit_version
|
48
57
|
ret
|
49
58
|
end
|
50
59
|
|
51
|
-
def self.
|
60
|
+
def self.module_name=(name)
|
52
61
|
@module_name = name
|
53
62
|
end
|
54
63
|
|
@@ -56,11 +65,19 @@ module HaveAPI
|
|
56
65
|
@module_name
|
57
66
|
end
|
58
67
|
|
59
|
-
def self.
|
68
|
+
def self.implicit_version=(v)
|
69
|
+
@implicit_version = v
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.implicit_version
|
73
|
+
@implicit_version
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.default_authenticate=(chain)
|
60
77
|
@default_auth = chain
|
61
78
|
end
|
62
79
|
|
63
80
|
def self.default_authenticate
|
64
|
-
@default_auth
|
81
|
+
@default_auth || []
|
65
82
|
end
|
66
83
|
end
|
data/lib/haveapi/common.rb
CHANGED
data/lib/haveapi/hooks.rb
CHANGED
@@ -46,18 +46,29 @@ module HaveAPI
|
|
46
46
|
module Hooks
|
47
47
|
# Register a hook defined by +klass+ with +name+.
|
48
48
|
# +klass+ is an instance of Class, that is class name, not it's instance.
|
49
|
-
|
49
|
+
# +opts+ is a hash and can have following keys:
|
50
|
+
# - desc - why this hook exists, when it's called
|
51
|
+
# - context - the context in which given blocks are called
|
52
|
+
# - args - hash of block arguments
|
53
|
+
# - initial - hash of initial values
|
54
|
+
# - ret - hash of return values
|
55
|
+
def self.register_hook(klass, name, opts = {})
|
50
56
|
classified = hook_classify(klass)
|
57
|
+
opts[:listeners] = []
|
51
58
|
|
52
59
|
@hooks ||= {}
|
53
60
|
@hooks[classified] ||= {}
|
54
|
-
@hooks[classified][name] =
|
61
|
+
@hooks[classified][name] = opts
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.hooks
|
65
|
+
@hooks
|
55
66
|
end
|
56
67
|
|
57
68
|
# Connect class hook defined in +klass+ with +name+ to +block+.
|
58
69
|
# +klass+ is a class name.
|
59
70
|
def self.connect_hook(klass, name, &block)
|
60
|
-
@hooks[hook_classify(klass)][name] << block
|
71
|
+
@hooks[hook_classify(klass)][name][:listeners] << block
|
61
72
|
end
|
62
73
|
|
63
74
|
# Connect instance hook from instance +klass+ with +name+ to +block+.
|
@@ -66,11 +77,11 @@ module HaveAPI
|
|
66
77
|
@hooks[klass] = {}
|
67
78
|
|
68
79
|
@hooks[klass.class].each do |k, v|
|
69
|
-
@hooks[klass][k] = []
|
80
|
+
@hooks[klass][k] = {listeners: []}
|
70
81
|
end
|
71
82
|
end
|
72
83
|
|
73
|
-
@hooks[klass][name] << block
|
84
|
+
@hooks[klass][name][:listeners] << block
|
74
85
|
end
|
75
86
|
|
76
87
|
# Call all blocks that are connected to hook in +klass+ with +name+.
|
@@ -92,7 +103,7 @@ module HaveAPI
|
|
92
103
|
|
93
104
|
catch(:stop) do
|
94
105
|
return initial unless @hooks[classified]
|
95
|
-
hooks = @hooks[classified][name]
|
106
|
+
hooks = @hooks[classified][name][:listeners]
|
96
107
|
return initial unless hooks
|
97
108
|
|
98
109
|
hooks.each do |hook|
|
@@ -122,8 +133,8 @@ module HaveAPI
|
|
122
133
|
module Hookable
|
123
134
|
module ClassMethods
|
124
135
|
# Register a hook named +name+.
|
125
|
-
def has_hook(name)
|
126
|
-
Hooks.register_hook(self.to_s, name)
|
136
|
+
def has_hook(name, opts = {})
|
137
|
+
Hooks.register_hook(self.to_s, name, opts)
|
127
138
|
end
|
128
139
|
|
129
140
|
# Connect +block+ to registered hook with +name+.
|
@@ -285,55 +285,94 @@ END
|
|
285
285
|
end
|
286
286
|
end
|
287
287
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
288
|
+
# Presence validator may have different meaning for model and controller.
|
289
|
+
# The attribute may be filled by other means than by controller and it is
|
290
|
+
# wrong to assume that the parameter must ALWAYS be sent by the client.
|
291
|
+
# Usually it would be needed only for Create and perhaps Update actions.
|
292
|
+
#
|
293
|
+
# handle ::ActiveRecord::Validations::PresenceValidator do |v|
|
294
|
+
# opts = { empty: false }
|
295
|
+
# opts[:message] = v.options[:message] if v.options[:message]
|
296
|
+
#
|
297
|
+
# validator(HaveAPI::Validators::Presence, :present, opts)
|
298
|
+
# end
|
295
299
|
|
296
300
|
handle ::ActiveModel::Validations::ExclusionValidator do |v|
|
297
|
-
|
301
|
+
opts = {
|
302
|
+
values: v.options[:in].map { |v| v }
|
303
|
+
}
|
304
|
+
opts[:message] = v.options[:message] if v.options[:message]
|
305
|
+
|
306
|
+
validator(:exclude, opts)
|
298
307
|
end
|
299
308
|
|
300
309
|
handle ::ActiveModel::Validations::FormatValidator do |v|
|
301
|
-
|
310
|
+
opts = {
|
311
|
+
rx: v.options[:with]
|
312
|
+
}
|
313
|
+
opts[:message] = v.options[:message] if v.options[:message]
|
314
|
+
|
315
|
+
validator(:format, opts)
|
302
316
|
end
|
303
317
|
|
304
318
|
handle ::ActiveModel::Validations::InclusionValidator do |v|
|
305
|
-
|
319
|
+
opts = {
|
320
|
+
values: v.options[:in].map { |v| v }
|
321
|
+
}
|
322
|
+
opts[:message] = v.options[:message] if v.options[:message]
|
323
|
+
|
324
|
+
validator(:include, opts)
|
306
325
|
end
|
307
326
|
|
308
327
|
handle ::ActiveModel::Validations::LengthValidator do |v|
|
309
|
-
|
328
|
+
opts = {}
|
329
|
+
opts[:min] = v.options[:minimum] if v.options[:minimum]
|
330
|
+
opts[:max] = v.options[:maximum] if v.options[:maximum]
|
331
|
+
opts[:equals] = v.options[:is] if v.options[:is]
|
332
|
+
opts[:message] = v.options[:message] if v.options[:message]
|
333
|
+
|
334
|
+
validator(:length, opts) unless opts.empty?
|
310
335
|
end
|
311
336
|
|
312
337
|
handle ::ActiveModel::Validations::NumericalityValidator do |v|
|
313
|
-
|
314
|
-
|
338
|
+
opts = {}
|
339
|
+
|
340
|
+
opts[:min] = v.options[:greater_than] + 1 if v.options[:greater_than]
|
341
|
+
opts[:min] = v.options[:greater_than_or_equal_to] if v.options[:greater_than_or_equal_to]
|
342
|
+
|
343
|
+
if v.options[:equal_to]
|
344
|
+
validator(accept: v.options[:equal_to])
|
345
|
+
next
|
346
|
+
end
|
347
|
+
|
348
|
+
opts[:max] = v.options[:less_than] - 1 if v.options[:less_than]
|
349
|
+
opts[:max] = v.options[:less_than_or_equal_to] if v.options[:less_than_or_equal_to]
|
350
|
+
|
351
|
+
opts[:odd] = true if v.options[:odd]
|
352
|
+
opts[:even] = true if v.options[:even]
|
353
|
+
|
354
|
+
opts[:message] = v.options[:message] if v.options[:message]
|
315
355
|
|
316
|
-
|
317
|
-
validator(v.options)
|
356
|
+
validator(:number, opts) unless opts.empty?
|
318
357
|
end
|
319
358
|
|
320
359
|
def initialize(params)
|
321
360
|
@params = params
|
322
361
|
end
|
323
362
|
|
324
|
-
def validator_for(param,
|
363
|
+
def validator_for(param, key, opts)
|
325
364
|
@params.each do |p|
|
326
365
|
next unless p.is_a?(::HaveAPI::Parameters::Param)
|
327
366
|
|
328
367
|
if p.db_name == param
|
329
|
-
p.add_validator(
|
368
|
+
p.add_validator(key, opts)
|
330
369
|
break
|
331
370
|
end
|
332
371
|
end
|
333
372
|
end
|
334
373
|
|
335
|
-
def validator(
|
336
|
-
validator_for(@attr,
|
374
|
+
def validator(key, opts)
|
375
|
+
validator_for(@attr, key, opts)
|
337
376
|
end
|
338
377
|
|
339
378
|
def translate(v)
|
@@ -32,8 +32,8 @@ module HaveAPI
|
|
32
32
|
@formatter.nil? ? false : true
|
33
33
|
end
|
34
34
|
|
35
|
-
def format(status, response, message = nil, errors = nil)
|
36
|
-
@formatter.format(header(status, response, message, errors))
|
35
|
+
def format(status, response, message = nil, errors = nil, version: true)
|
36
|
+
@formatter.format(header(status, response, message, errors, version))
|
37
37
|
end
|
38
38
|
|
39
39
|
def error(msg)
|
@@ -45,13 +45,16 @@ module HaveAPI
|
|
45
45
|
end
|
46
46
|
|
47
47
|
protected
|
48
|
-
def header(status, response, message = nil, errors = nil)
|
49
|
-
{
|
48
|
+
def header(status, response, message = nil, errors = nil, version)
|
49
|
+
ret = {}
|
50
|
+
ret[:version] = HaveAPI::PROTOCOL_VERSION if version
|
51
|
+
ret.update({
|
50
52
|
status: status,
|
51
53
|
response: response,
|
52
54
|
message: message,
|
53
55
|
errors: errors
|
54
|
-
}
|
56
|
+
})
|
57
|
+
ret
|
55
58
|
end
|
56
59
|
end
|
57
60
|
end
|
data/lib/haveapi/params/param.rb
CHANGED
@@ -1,22 +1,22 @@
|
|
1
1
|
module HaveAPI::Parameters
|
2
2
|
class Param
|
3
|
+
ATTRIBUTES = %i(label desc type db_name default fill clean)
|
4
|
+
|
3
5
|
attr_reader :name, :label, :desc, :type, :default
|
4
6
|
|
5
|
-
def initialize(name,
|
6
|
-
choices: nil, db_name: nil, default: :_nil, fill: false,
|
7
|
-
clean: nil)
|
8
|
-
@required = required
|
7
|
+
def initialize(name, args = {})
|
9
8
|
@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
|
9
|
+
@label = args.delete(:label) || name.to_s.capitalize
|
17
10
|
@layout = :custom
|
18
|
-
|
19
|
-
|
11
|
+
|
12
|
+
ATTRIBUTES.each do |attr|
|
13
|
+
instance_variable_set("@#{attr}", args.delete(attr))
|
14
|
+
end
|
15
|
+
|
16
|
+
@type ||= String
|
17
|
+
|
18
|
+
@validators = HaveAPI::ValidatorChain.new(args) unless args.empty?
|
19
|
+
fail "unused arguments #{args}" unless args.empty?
|
20
20
|
end
|
21
21
|
|
22
22
|
def db_name
|
@@ -24,7 +24,7 @@ module HaveAPI::Parameters
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def required?
|
27
|
-
@required
|
27
|
+
@validators ? @validators.required? : false
|
28
28
|
end
|
29
29
|
|
30
30
|
def optional?
|
@@ -35,33 +35,36 @@ module HaveAPI::Parameters
|
|
35
35
|
@fill
|
36
36
|
end
|
37
37
|
|
38
|
-
def add_validator(v)
|
39
|
-
@validators.update(v)
|
40
|
-
end
|
41
|
-
|
42
|
-
def validators
|
43
|
-
@validators
|
44
|
-
end
|
45
|
-
|
46
38
|
def describe(context)
|
47
39
|
{
|
48
40
|
required: required?,
|
49
41
|
label: @label,
|
50
42
|
description: @desc,
|
51
43
|
type: @type ? @type.to_s : String.to_s,
|
52
|
-
|
53
|
-
validators: @validators,
|
44
|
+
validators: @validators ? @validators.describe : {},
|
54
45
|
default: @default
|
55
46
|
}
|
56
47
|
end
|
57
48
|
|
49
|
+
def add_validator(k, v)
|
50
|
+
@validators ||= HaveAPI::ValidatorChain.new({})
|
51
|
+
@validators.add_or_replace(k, v)
|
52
|
+
end
|
53
|
+
|
58
54
|
def patch(attrs)
|
59
|
-
attrs.each
|
55
|
+
attrs.each do |k, v|
|
56
|
+
if ATTRIBUTES.include?(k)
|
57
|
+
instance_variable_set("@#{k}", v)
|
58
|
+
|
59
|
+
else
|
60
|
+
add_validator(k, v)
|
61
|
+
end
|
62
|
+
end
|
60
63
|
end
|
61
64
|
|
62
65
|
def clean(raw)
|
63
66
|
return instance_exec(raw, &@clean) if @clean
|
64
|
-
|
67
|
+
|
65
68
|
val = if raw.nil?
|
66
69
|
@default
|
67
70
|
|
@@ -86,22 +89,13 @@ module HaveAPI::Parameters
|
|
86
89
|
raw
|
87
90
|
end
|
88
91
|
|
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
92
|
val
|
103
93
|
end
|
104
94
|
|
95
|
+
def validate(v, params)
|
96
|
+
@validators ? @validators.validate(v, params) : true
|
97
|
+
end
|
98
|
+
|
105
99
|
def format_output(v)
|
106
100
|
if @type == ::Datetime && v.is_a?(Time)
|
107
101
|
v.iso8601
|
@@ -37,6 +37,10 @@ module HaveAPI::Parameters
|
|
37
37
|
@resource::Show
|
38
38
|
end
|
39
39
|
|
40
|
+
def show_index
|
41
|
+
@resource::Index
|
42
|
+
end
|
43
|
+
|
40
44
|
def describe(context)
|
41
45
|
val_url = context.url_for(
|
42
46
|
@resource::Show,
|
@@ -71,6 +75,18 @@ module HaveAPI::Parameters
|
|
71
75
|
}
|
72
76
|
end
|
73
77
|
|
78
|
+
def validate_build_output
|
79
|
+
%i(value_id value_label).each do |name|
|
80
|
+
v = instance_variable_get("@#{name}")
|
81
|
+
|
82
|
+
[show_action, show_index].each do |klass|
|
83
|
+
next unless klass.instance_variable_get('@output')[v].nil?
|
84
|
+
|
85
|
+
fail "association to '#{@resource}': value_label '#{v}' is not an output parameter of '#{klass}'"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
74
90
|
def patch(attrs)
|
75
91
|
attrs.each { |k, v| instance_variable_set("@#{k}", v) }
|
76
92
|
end
|
@@ -81,6 +97,10 @@ module HaveAPI::Parameters
|
|
81
97
|
).input_clean(@resource.model, raw, @extra)
|
82
98
|
end
|
83
99
|
|
100
|
+
def validate(v, params)
|
101
|
+
true
|
102
|
+
end
|
103
|
+
|
84
104
|
def format_output(v)
|
85
105
|
v
|
86
106
|
end
|
data/lib/haveapi/params.rb
CHANGED
@@ -187,6 +187,14 @@ module HaveAPI
|
|
187
187
|
ret
|
188
188
|
end
|
189
189
|
|
190
|
+
def validate_build
|
191
|
+
m = :"validate_build_#{@direction}"
|
192
|
+
|
193
|
+
@params.each do |p|
|
194
|
+
p.send(m) if p.respond_to?(m)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
190
198
|
# First step of validation. Check if input is in correct namespace
|
191
199
|
# and has a correct layout.
|
192
200
|
def check_layout(params)
|
@@ -207,14 +215,15 @@ module HaveAPI
|
|
207
215
|
# convert params to correct data types, set default values if necessary.
|
208
216
|
def validate(params)
|
209
217
|
errors = {}
|
210
|
-
|
218
|
+
|
211
219
|
layout_aware(params) do |input|
|
220
|
+
# First run - coerce values to correct types
|
212
221
|
@params.each do |p|
|
213
222
|
if p.required? && input[p.name].nil?
|
214
223
|
errors[p.name] = ['required parameter missing']
|
215
224
|
next
|
216
225
|
end
|
217
|
-
|
226
|
+
|
218
227
|
unless input.has_key?(p.name)
|
219
228
|
input[p.name] = p.default if p.respond_to?(:fill?) && p.fill?
|
220
229
|
next
|
@@ -228,11 +237,24 @@ module HaveAPI
|
|
228
237
|
errors[p.name] << e.message
|
229
238
|
next
|
230
239
|
end
|
231
|
-
|
240
|
+
|
232
241
|
input[p.name] = cleaned if cleaned != :_nil
|
233
242
|
end
|
234
|
-
|
243
|
+
|
244
|
+
# Second run - validate parameters
|
245
|
+
@params.each do |p|
|
246
|
+
next if errors.has_key?(p.name)
|
247
|
+
next if input[p.name].nil?
|
235
248
|
|
249
|
+
res = p.validate(input[p.name], input)
|
250
|
+
|
251
|
+
unless res === true
|
252
|
+
errors[p.name] ||= []
|
253
|
+
errors[p.name].concat(res)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
236
258
|
unless errors.empty?
|
237
259
|
raise ValidationError.new('input parameters not valid', errors)
|
238
260
|
end
|
Binary file
|
data/lib/haveapi/resource.rb
CHANGED
@@ -25,13 +25,8 @@ module HaveAPI
|
|
25
25
|
constants.select do |c|
|
26
26
|
obj = const_get(c)
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
yield obj
|
31
|
-
end
|
32
|
-
|
33
|
-
rescue NoMethodError
|
34
|
-
next
|
28
|
+
if obj.respond_to?(:obj_type) && obj.obj_type == :action
|
29
|
+
yield obj
|
35
30
|
end
|
36
31
|
end
|
37
32
|
end
|