haveapi 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +12 -0
  3. data/Gemfile +10 -10
  4. data/README.md +19 -12
  5. data/Rakefile +23 -0
  6. data/doc/hooks.erb +35 -0
  7. data/doc/index.md +1 -0
  8. data/doc/json-schema.erb +369 -0
  9. data/doc/protocol.md +178 -38
  10. data/doc/protocol.plantuml +220 -0
  11. data/haveapi.gemspec +6 -10
  12. data/lib/haveapi/action.rb +35 -6
  13. data/lib/haveapi/api.rb +22 -5
  14. data/lib/haveapi/common.rb +7 -0
  15. data/lib/haveapi/exceptions.rb +7 -0
  16. data/lib/haveapi/hooks.rb +19 -8
  17. data/lib/haveapi/model_adapters/active_record.rb +58 -19
  18. data/lib/haveapi/output_formatter.rb +8 -5
  19. data/lib/haveapi/output_formatters/json.rb +6 -1
  20. data/lib/haveapi/params/param.rb +33 -39
  21. data/lib/haveapi/params/resource.rb +20 -0
  22. data/lib/haveapi/params.rb +26 -4
  23. data/lib/haveapi/public/doc/protocol.png +0 -0
  24. data/lib/haveapi/resource.rb +2 -7
  25. data/lib/haveapi/server.rb +87 -26
  26. data/lib/haveapi/tasks/hooks.rb +3 -0
  27. data/lib/haveapi/tasks/yard.rb +12 -0
  28. data/lib/haveapi/validator.rb +134 -0
  29. data/lib/haveapi/validator_chain.rb +99 -0
  30. data/lib/haveapi/validators/acceptance.rb +38 -0
  31. data/lib/haveapi/validators/confirmation.rb +46 -0
  32. data/lib/haveapi/validators/custom.rb +21 -0
  33. data/lib/haveapi/validators/exclusion.rb +38 -0
  34. data/lib/haveapi/validators/format.rb +42 -0
  35. data/lib/haveapi/validators/inclusion.rb +42 -0
  36. data/lib/haveapi/validators/length.rb +71 -0
  37. data/lib/haveapi/validators/numericality.rb +104 -0
  38. data/lib/haveapi/validators/presence.rb +40 -0
  39. data/lib/haveapi/version.rb +2 -1
  40. data/lib/haveapi/views/doc_sidebars/json-schema.erb +7 -0
  41. data/lib/haveapi/views/main_layout.erb +11 -0
  42. data/lib/haveapi/views/version_page.erb +26 -3
  43. data/lib/haveapi.rb +7 -4
  44. 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
- r.version.is_a?(Array) ? r.version.include?(v) : (r.version == v || r.version == :all)
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.get_versions(module_name)
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.set_module_name(name)
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.set_default_authenticate(chain)
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
@@ -31,6 +31,13 @@ module HaveAPI
31
31
  subclass.custom_attrs << attr
32
32
  end
33
33
  end
34
+
35
+ def check_build(msg)
36
+ yield
37
+
38
+ rescue => e
39
+ raise BuildError.new(msg, e)
40
+ end
34
41
  end
35
42
 
36
43
  has_attr :obj_type
@@ -0,0 +1,7 @@
1
+ require 'nesty'
2
+
3
+ module HaveAPI
4
+ class BuildError < StandardError
5
+ include Nesty::NestedError
6
+ end
7
+ end
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
- def self.register_hook(klass, name)
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
- 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
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
- validator(v.options)
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
- validator({format: {with_source: v.options[:with].source}.update(v.options)})
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
- validator(v.options)
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
- validator(v.options)
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
- validator(v.options)
314
- end
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
- handle ::ActiveRecord::Validations::UniquenessValidator do |v|
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, v)
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(v)
368
+ p.add_validator(key, opts)
330
369
  break
331
370
  end
332
371
  end
333
372
  end
334
373
 
335
- def validator(v)
336
- validator_for(@attr, v)
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
@@ -3,7 +3,12 @@ module HaveAPI::OutputFormatters
3
3
  handle 'application/json'
4
4
 
5
5
  def format(response)
6
- JSON.pretty_generate(response)
6
+ if ENV['RACK_ENV'] == 'development'
7
+ JSON.pretty_generate(response)
8
+
9
+ else
10
+ JSON.generate(response)
11
+ end
7
12
  end
8
13
  end
9
14
  end
@@ -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, required: nil, label: nil, desc: nil, type: nil,
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
- @validators = {}
19
- @clean = clean
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
- choices: @choices,
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 { |k, v| instance_variable_set("@#{k}", v) }
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
@@ -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
- end
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
@@ -25,13 +25,8 @@ module HaveAPI
25
25
  constants.select do |c|
26
26
  obj = const_get(c)
27
27
 
28
- begin
29
- if obj.obj_type == :action
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