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
@@ -1,3 +1,6 @@
1
+ require 'erb'
2
+ require 'redcarpet'
3
+
1
4
  module HaveAPI
2
5
  class Server
3
6
  attr_reader :root, :routes, :module_name, :auth_chain, :versions, :default_version,
@@ -7,7 +10,11 @@ module HaveAPI
7
10
 
8
11
  # Called after the user was authenticated (or not). The block is passed
9
12
  # current user object or nil as an argument.
10
- has_hook :post_authenticated
13
+ has_hook :post_authenticated,
14
+ desc: 'Called after the user was authenticated',
15
+ args: {
16
+ current_user: 'object returned by the authentication backend',
17
+ }
11
18
 
12
19
  module ServerHelpers
13
20
  def authenticate!(v)
@@ -51,7 +58,7 @@ module HaveAPI
51
58
  def report_error(code, headers, msg)
52
59
  @halted = true
53
60
  content_type @formatter.content_type, charset: 'utf-8'
54
- halt code, headers, @formatter.format(false, nil, msg)
61
+ halt code, headers, @formatter.format(false, nil, msg, version: false)
55
62
  end
56
63
 
57
64
  def root
@@ -90,7 +97,7 @@ module HaveAPI
90
97
  @versions ||= []
91
98
 
92
99
  if v == :all
93
- @versions = HaveAPI.get_versions(@module_name)
100
+ @versions = HaveAPI.versions(@module_name)
94
101
  elsif v.is_a?(Array)
95
102
  @versions += v
96
103
  @versions.uniq!
@@ -101,7 +108,7 @@ module HaveAPI
101
108
  end
102
109
 
103
110
  # Set default version of API.
104
- def set_default_version(v)
111
+ def default_version=(v)
105
112
  @default_version = v
106
113
  end
107
114
 
@@ -152,8 +159,11 @@ module HaveAPI
152
159
  @sinatra.get @root do
153
160
  authenticated?(settings.api_server.default_version)
154
161
 
155
- @api = settings.api_server.describe(Context.new(settings.api_server, user: current_user,
156
- params: params))
162
+ @api = settings.api_server.describe(Context.new(
163
+ settings.api_server,
164
+ user: current_user,
165
+ params: params
166
+ ))
157
167
 
158
168
  content_type 'text/html'
159
169
  erb :index, layout: :main_layout
@@ -166,16 +176,24 @@ module HaveAPI
166
176
 
167
177
  case params[:describe]
168
178
  when 'versions'
169
- ret = {versions: settings.api_server.versions,
170
- default: settings.api_server.default_version}
179
+ ret = {
180
+ versions: settings.api_server.versions,
181
+ default: settings.api_server.default_version
182
+ }
171
183
 
172
184
  when 'default'
173
- ret = settings.api_server.describe_version(Context.new(settings.api_server, version: settings.api_server.default_version,
174
- user: current_user, params: params))
185
+ ret = settings.api_server.describe_version(Context.new(
186
+ settings.api_server,
187
+ version: settings.api_server.default_version,
188
+ user: current_user, params: params
189
+ ))
175
190
 
176
191
  else
177
- ret = settings.api_server.describe(Context.new(settings.api_server, user: current_user,
178
- params: params))
192
+ ret = settings.api_server.describe(Context.new(
193
+ settings.api_server,
194
+ user: current_user,
195
+ params: params
196
+ ))
179
197
  end
180
198
 
181
199
  @formatter.format(true, ret)
@@ -195,6 +213,14 @@ module HaveAPI
195
213
  GitHub::Markdown.render(File.new(settings.views + '/../../../README.md').read)
196
214
  end
197
215
  end
216
+
217
+ @sinatra.get "#{@root}doc/json-schema" do
218
+ content_type 'text/html'
219
+ erb :doc_layout, layout: :main_layout do
220
+ @content = erb :'../../../doc/json-schema'
221
+ @sidebar = erb :'doc_sidebars/json-schema'
222
+ end
223
+ end
198
224
 
199
225
  @sinatra.get %r{#{@root}doc/([^\.]+)[\.md]?} do |f|
200
226
  content_type 'text/html'
@@ -244,8 +270,12 @@ module HaveAPI
244
270
  authenticated?(v)
245
271
 
246
272
  @v = v
247
- @help = settings.api_server.describe_version(Context.new(settings.api_server, version: v,
248
- user: current_user, params: params))
273
+ @help = settings.api_server.describe_version(Context.new(
274
+ settings.api_server,
275
+ version: v,
276
+ user: current_user,
277
+ params: params
278
+ ))
249
279
  content_type 'text/html'
250
280
  erb :doc_layout, layout: :main_layout do
251
281
  @content = erb :version_page
@@ -257,13 +287,29 @@ module HaveAPI
257
287
  access_control
258
288
  authenticated?(v)
259
289
 
260
- @formatter.format(true, settings.api_server.describe_version(Context.new(settings.api_server, version: v,
261
- user: current_user, params: params)))
290
+ @formatter.format(true, settings.api_server.describe_version(Context.new(
291
+ settings.api_server,
292
+ version: v,
293
+ user: current_user,
294
+ params: params
295
+ )))
262
296
  end
263
297
 
264
298
  HaveAPI.get_version_resources(@module_name, v).each do |resource|
265
299
  mount_resource(prefix, v, resource, @routes[v][:resources])
266
300
  end
301
+
302
+ validate_resources(@routes[v][:resources])
303
+ end
304
+
305
+ def validate_resources(resources)
306
+ resources.each_value do |r|
307
+ r[:actions].each_key do |a|
308
+ a.validate_build
309
+ end
310
+
311
+ validate_resources(r[:resources])
312
+ end
267
313
  end
268
314
 
269
315
  def mount_resource(prefix, v, resource, hash)
@@ -271,7 +317,10 @@ module HaveAPI
271
317
 
272
318
  resource.routes(prefix).each do |route|
273
319
  if route.is_a?(Hash)
274
- hash[resource][:resources][route.keys.first] = mount_nested_resource(v, route.values.first)
320
+ hash[resource][:resources][route.keys.first] = mount_nested_resource(
321
+ v,
322
+ route.values.first
323
+ )
275
324
 
276
325
  else
277
326
  hash[resource][:actions][route.action] = route.url
@@ -315,10 +364,15 @@ module HaveAPI
315
364
  report_error(400, {}, 'Bad JSON syntax')
316
365
  end
317
366
 
318
- action = route.action.new(request, v, params, body, Context.new(settings.api_server, version: v,
319
- action: route.action, url: route.url,
320
- params: params,
321
- user: current_user, endpoint: true))
367
+ action = route.action.new(request, v, params, body, Context.new(
368
+ settings.api_server,
369
+ version: v,
370
+ action: route.action,
371
+ url: route.url,
372
+ params: params,
373
+ user: current_user,
374
+ endpoint: true
375
+ ))
322
376
 
323
377
  unless action.authorized?(current_user)
324
378
  report_error(403, {}, 'Access denied. Insufficient permissions.')
@@ -330,7 +384,8 @@ module HaveAPI
330
384
  status,
331
385
  status ? reply : nil,
332
386
  !status ? reply : nil,
333
- errors
387
+ errors,
388
+ version: false
334
389
  )
335
390
  end
336
391
 
@@ -343,10 +398,16 @@ module HaveAPI
343
398
  authenticate!(v) if route.action.auth
344
399
 
345
400
  begin
346
- desc = route.action.describe(Context.new(settings.api_server, version: v,
347
- action: route.action, url: route.url,
348
- args: args, params: params,
349
- user: current_user, endpoint: true))
401
+ desc = route.action.describe(Context.new(
402
+ settings.api_server,
403
+ version: v,
404
+ action: route.action,
405
+ url: route.url,
406
+ args: args,
407
+ params: params,
408
+ user: current_user,
409
+ endpoint: true
410
+ ))
350
411
 
351
412
  unless desc
352
413
  report_error(403, {}, 'Access denied. Insufficient permissions.')
@@ -0,0 +1,3 @@
1
+ def document_hooks(file = 'doc/Hooks.md')
2
+ render_doc_file('doc/hooks.erb', 'doc/Hooks.md')
3
+ end
@@ -0,0 +1,12 @@
1
+ require 'haveapi/tasks/hooks'
2
+
3
+ def render_doc_file(src, dst)
4
+ src = File.join(File.dirname(__FILE__), '..', '..', '..', src)
5
+
6
+ Proc.new do
7
+ File.write(
8
+ dst,
9
+ ERB.new(File.read(src), 0).result(binding)
10
+ )
11
+ end
12
+ end
@@ -0,0 +1,134 @@
1
+ module HaveAPI
2
+ # Validators are stored in this module.
3
+ module Validators ; end
4
+
5
+ # Base class for all validators.
6
+ #
7
+ # All validators can have a short and a full form. The short form is used
8
+ # when default configuration is sufficient. Custom settings can be set using
9
+ # the full form.
10
+ #
11
+ # The short form means the validator is configured as +<option> => <single value>+.
12
+ # The full form is +<option> => { hash with configuration options }+.
13
+ #
14
+ # It is up to each validator what exactly the short form means and what options
15
+ # can be set. Specify only those options that you wish to override. The only
16
+ # common option is +message+ - the error message sent to the client if the provided
17
+ # value did not pass the validator.
18
+ #
19
+ # The +message+ can contain +%{value}+, which is replaced by the actual value
20
+ # that did not pass the validator.
21
+ class Validator
22
+ class << self
23
+ # Set validator name used in API documentation.
24
+ def name(v = nil)
25
+ if v
26
+ @name = v
27
+
28
+ else
29
+ @name
30
+ end
31
+ end
32
+
33
+ # Specify options this validator takes from the parameter definition.
34
+ def takes(*opts)
35
+ @takes = opts
36
+ end
37
+
38
+ # True if this validator uses any of options in hash +opts+.
39
+ def use?(opts)
40
+ !(opts.keys & @takes).empty?
41
+ end
42
+
43
+ # Use the validator on given set of options in hash +opts+. Used
44
+ # options are removed from +opts+.
45
+ def use(opts)
46
+ keys = opts.keys & @takes
47
+
48
+ fail 'too many keys' if keys.size > 1
49
+ new(keys.first, opts.delete(keys.first))
50
+ end
51
+ end
52
+
53
+ attr_accessor :message, :params
54
+
55
+ def initialize(key, opts)
56
+ reconfigure(key, opts)
57
+ end
58
+
59
+ def reconfigure(key, opts)
60
+ @key = key
61
+ @opts = opts
62
+ setup
63
+ end
64
+
65
+ def useful?
66
+ @useful.nil? ? true : @useful
67
+ end
68
+
69
+ # Validators should be configured by the given options. This method may be
70
+ # called multiple times, if the validator is reconfigured after it was
71
+ # created.
72
+ def setup
73
+ raise NotImplementedError
74
+ end
75
+
76
+ # Return a hash documenting this validator.
77
+ def describe
78
+ raise NotImplementedError
79
+ end
80
+
81
+ # Return true if the value is valid.
82
+ def valid?(v)
83
+ raise NotImplementedError
84
+ end
85
+
86
+ # Calls method valid?, but before calling it sets instance variable
87
+ # +@params+. It contains of hash of all other parameters. The validator
88
+ # may use this information as it will.
89
+ def validate(v, params)
90
+ @params = params
91
+ ret = valid?(v)
92
+ @params = nil
93
+ ret
94
+ end
95
+
96
+ protected
97
+ # This method has three modes of function.
98
+ #
99
+ # 1. If +v+ is nil, it returns +@opts+. It is used if +@opts+ is not a hash
100
+ # but a single value - abbreviation if we're ok with default settings
101
+ # for given validator.
102
+ # 2. If +v+ is not nil and +@opts+ is not a hash, it returns +default+
103
+ # 3. If +v+ is not nil and +@opts+ is a hash and +@opts[v]+ is not nil, it is
104
+ # returned. Otherwise the +default+ is returned.
105
+ def take(v = nil, default = nil)
106
+ if v.nil?
107
+ @opts
108
+
109
+ else
110
+ return default unless @opts.is_a?(::Hash)
111
+ return @opts[v] unless @opts[v].nil?
112
+ default
113
+ end
114
+ end
115
+
116
+ # Declare validator as useless. Such validator does not do anything and can
117
+ # be removed from validator chain. Validator can become useless when it's configuration
118
+ # makes it so.
119
+ def useless
120
+ @useful = false
121
+ end
122
+
123
+ # Returns true if +@opts+ is not a hash.
124
+ def simple?
125
+ !@opts.is_a?(::Hash)
126
+ end
127
+
128
+ # Returns the name of the option given to this validator. It may bear some
129
+ # meaning to the validator.
130
+ def opt
131
+ @key
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,99 @@
1
+ module HaveAPI
2
+ # A chain of validators for one input parameter.
3
+ class ValidatorChain
4
+ def initialize(args)
5
+ @validators = []
6
+ @required = false
7
+
8
+ find_validators(args) do |validator|
9
+ obj = validator.use(args)
10
+ next unless obj.useful?
11
+ @required = true if obj.is_a?(Validators::Presence)
12
+ @validators << obj
13
+ end
14
+ end
15
+
16
+ # Adds validator that takes option +name+ with configuration in +opt+.
17
+ # If such validator already exists, it is reconfigured with newly provided
18
+ # +opt+.
19
+ #
20
+ # If +opt+ is +nil+, the validator is removed.
21
+ def add_or_replace(name, opt)
22
+ args = { name => opt }
23
+
24
+ unless v_class = find_validator(args)
25
+ fail "validator for '#{name}' not found"
26
+ end
27
+
28
+ exists = @validators.detect { |v| v.is_a?(v_class) }
29
+ obj = exists
30
+
31
+ if exists
32
+ if opt.nil?
33
+ @validators.delete(exists)
34
+
35
+ else
36
+ exists.reconfigure(name, opt)
37
+ @validators.delete(exists) unless exists.useful?
38
+ end
39
+
40
+ else
41
+ obj = v_class.use(args)
42
+ @validators << obj if obj.useful?
43
+ end
44
+
45
+ if v_class == Validators::Presence
46
+ @required = !opt.nil? && obj.useful? ? true : false
47
+ end
48
+ end
49
+
50
+ # Returns true if validator Validators::Presence is used.
51
+ def required?
52
+ @required
53
+ end
54
+
55
+ def describe
56
+ ret = {}
57
+ @validators.each do |v|
58
+ ret[v.class.name] = v.describe
59
+ end
60
+
61
+ ret
62
+ end
63
+
64
+ # Validate +value+ using all configured validators. It returns
65
+ # either +true+ if the value passed all validators or an array
66
+ # of errors.
67
+ def validate(value, params)
68
+ ret = []
69
+
70
+ @validators.each do |validator|
71
+ next if validator.validate(value, params)
72
+ ret << validator.message % {
73
+ value: value
74
+ }
75
+ end
76
+
77
+ ret.empty? ? true : ret
78
+ end
79
+
80
+ protected
81
+ def find_validator(args)
82
+ HaveAPI::Validators.constants.select do |v|
83
+ validator = HaveAPI::Validators.const_get(v)
84
+
85
+ return validator if validator.use?(args)
86
+ end
87
+
88
+ nil
89
+ end
90
+
91
+ def find_validators(args)
92
+ HaveAPI::Validators.constants.select do |v|
93
+ validator = HaveAPI::Validators.const_get(v)
94
+
95
+ yield(validator) if validator.use?(args)
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,38 @@
1
+ module HaveAPI
2
+ # Accepts a single configured value.
3
+ #
4
+ # Short form:
5
+ # string :param, accept: 'value'
6
+ #
7
+ # Full form:
8
+ # string :param, accept: {
9
+ # value: 'value',
10
+ # message: 'the error message'
11
+ # }
12
+ class Validators::Acceptance < Validator
13
+ name :accept
14
+ takes :accept
15
+
16
+ def setup
17
+ if simple?
18
+ @value = take
19
+
20
+ else
21
+ @value = take(:value)
22
+ end
23
+
24
+ @message = take(:message, "has to be #{@value}")
25
+ end
26
+
27
+ def describe
28
+ {
29
+ value: @value,
30
+ message: @message,
31
+ }
32
+ end
33
+
34
+ def valid?(v)
35
+ v == @value
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,46 @@
1
+ module HaveAPI
2
+ # Checks that two parameters are equal or not equal.
3
+ #
4
+ # Short form:
5
+ # string :param, confirm: :other_parameter
6
+ #
7
+ # Full form:
8
+ # string :param, confirm: {
9
+ # param: :other_parameter,
10
+ # equal: true/false,
11
+ # message: 'the error message'
12
+ # }
13
+ #
14
+ # +equal+ defaults to +true+.
15
+ class Validators::Confirmation < Validator
16
+ name :confirm
17
+ takes :confirm
18
+
19
+ def setup
20
+ @param = simple? ? take : take(:param)
21
+ @equal = take(:equal, true)
22
+ @message = take(
23
+ :message,
24
+ @equal ? "must be the same as #{@param}"
25
+ : "must be different from #{@param}"
26
+ )
27
+ end
28
+
29
+ def describe
30
+ {
31
+ equal: @equal ? true : false,
32
+ parameter: @param,
33
+ message: @message,
34
+ }
35
+ end
36
+
37
+ def valid?(v)
38
+ if @equal
39
+ v == params[@param]
40
+
41
+ else
42
+ v != params[@param]
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,21 @@
1
+ module HaveAPI
2
+ # Custom validator. It has only a short form, taking the description
3
+ # of the validator. This validator passes every value. It is up to the
4
+ # developer to implement the validation in HaveAPI::Action.exec.
5
+ class Validators::Custom < Validator
6
+ name :custom
7
+ takes :validate
8
+
9
+ def setup
10
+ @desc = take
11
+ end
12
+
13
+ def describe
14
+ @desc
15
+ end
16
+
17
+ def valid?(v)
18
+ true
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,38 @@
1
+ module HaveAPI
2
+ # Checks that the value is not reserved.
3
+ #
4
+ # Short form:
5
+ # string :param, exclude: %i(one two three)
6
+ #
7
+ # Full form:
8
+ # string :param, exclude: {
9
+ # values: %i(one two three),
10
+ # message: 'the error message'
11
+ # }
12
+ #
13
+ # In this case, the value could be anything but +one+, +two+ or
14
+ # +three+.
15
+ class Validators::Exclusion < Validator
16
+ name :exclude
17
+ takes :exclude
18
+
19
+ def setup
20
+ @values = (simple? ? take : take(:values)).map! do |v|
21
+ v.is_a?(::Symbol) ? v.to_s : v
22
+ end
23
+
24
+ @message = take(:message, '%{value} cannot be used')
25
+ end
26
+
27
+ def describe
28
+ {
29
+ values: @values,
30
+ message: @message,
31
+ }
32
+ end
33
+
34
+ def valid?(v)
35
+ !@values.include?(v)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,42 @@
1
+ module HaveAPI
2
+ # Checks that the value is or is not in specified format.
3
+ #
4
+ # Short form:
5
+ # string :param, format: /^[a-z0-9]+$/
6
+ #
7
+ # Full form:
8
+ # string :param, format: {
9
+ # rx: /^[a-z0-9]+$/,
10
+ # match: true/false,
11
+ # message: 'the error message'
12
+ # }
13
+ class Validators::Format < Validator
14
+ name :format
15
+ takes :format
16
+
17
+ def setup
18
+ @rx = simple? ? take : take(:rx)
19
+ @match = take(:match, true)
20
+ @desc = take(:desc)
21
+ @message = take(:message, @desc || '%{value} is not in a valid format')
22
+ end
23
+
24
+ def describe
25
+ {
26
+ rx: @rx.source,
27
+ match: @match,
28
+ description: @desc,
29
+ message: @message,
30
+ }
31
+ end
32
+
33
+ def valid?(v)
34
+ if @match
35
+ @rx.match(v) ? true : false
36
+
37
+ else
38
+ @rx.match(v) ? false : true
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,42 @@
1
+ module HaveAPI
2
+ # Checks that the value is from given set of allowed values.
3
+ #
4
+ # Short form:
5
+ # string :param, choices: %i(one two three)
6
+ #
7
+ # Full form:
8
+ # string :param, choices: {
9
+ # values: %i(one two three),
10
+ # message: 'the error message'
11
+ # }
12
+ #
13
+ # Option +choices+ is an alias to +include+.
14
+ class Validators::Inclusion < Validator
15
+ name :include
16
+ takes :choices, :include
17
+
18
+ def setup
19
+ @values = (simple? ? take : take(:values)).map! do |v|
20
+ v.is_a?(::Symbol) ? v.to_s : v
21
+ end
22
+
23
+ @message = take(:message, '%{value} cannot be used')
24
+ end
25
+
26
+ def describe
27
+ {
28
+ values: @values,
29
+ message: @message,
30
+ }
31
+ end
32
+
33
+ def valid?(v)
34
+ if @values.is_a?(::Hash)
35
+ @values.has_key?(v)
36
+
37
+ else
38
+ @values.include?(v)
39
+ end
40
+ end
41
+ end
42
+ end