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.
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