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