haveapi 0.3.2 → 0.4.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 +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
|