haveapi 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +11 -0
  3. data/Rakefile +1 -0
  4. data/haveapi.gemspec +1 -1
  5. data/lib/haveapi/authentication/chain.rb +4 -0
  6. data/lib/haveapi/hooks.rb +68 -11
  7. data/lib/haveapi/model_adapters/active_record.rb +1 -1
  8. data/lib/haveapi/{params/param.rb → parameters/typed.rb} +4 -1
  9. data/lib/haveapi/params.rb +2 -2
  10. data/lib/haveapi/spec/api_builder.rb +75 -0
  11. data/lib/haveapi/spec/api_response.rb +41 -0
  12. data/lib/haveapi/spec/helpers.rb +5 -99
  13. data/lib/haveapi/spec/mock_action.rb +32 -0
  14. data/lib/haveapi/spec/spec_methods.rb +121 -0
  15. data/lib/haveapi/version.rb +1 -1
  16. data/lib/haveapi/views/main_layout.erb +3 -1
  17. data/lib/haveapi.rb +1 -1
  18. data/spec/action/dsl_spec.rb +199 -0
  19. data/spec/authorization_spec.rb +113 -0
  20. data/spec/common_spec.rb +47 -0
  21. data/spec/documentation_spec.rb +29 -0
  22. data/spec/envelope_spec.rb +33 -0
  23. data/spec/hooks_spec.rb +146 -0
  24. data/spec/parameters/typed_spec.rb +93 -0
  25. data/spec/params_spec.rb +190 -0
  26. data/spec/resource_spec.rb +63 -0
  27. data/spec/spec_helper.rb +21 -0
  28. data/spec/validators/acceptance_spec.rb +29 -0
  29. data/spec/validators/confirmation_spec.rb +46 -0
  30. data/spec/validators/custom_spec.rb +6 -0
  31. data/spec/validators/exclusion_spec.rb +32 -0
  32. data/spec/validators/format_spec.rb +54 -0
  33. data/spec/validators/inclusion_spec.rb +43 -0
  34. data/spec/validators/length_spec.rb +45 -0
  35. data/spec/validators/numericality_spec.rb +70 -0
  36. data/spec/validators/presence_spec.rb +47 -0
  37. metadata +27 -4
  38. /data/lib/haveapi/{params → parameters}/resource.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f19eb81f8b3f54654715dfaa52c85e17675860a6
4
- data.tar.gz: 5b5013c9f1b52af5a55935fd44d78a845d2fdd7d
3
+ metadata.gz: 00a5f09608d0287b5187d60b5e5f8528b8077c33
4
+ data.tar.gz: 728e896638ef103f0d62db8aea3d0a26cae3e818
5
5
  SHA512:
6
- metadata.gz: 8b1df715121edd42ba79af47601e2d5daa87226a2a4cdf887d6d54b046e5a287e17451d8950a8c9ed722ccfb8eed4cd0e6343b1fd6aa574db4fa39515c41c048
7
- data.tar.gz: a2c9fec65f9ab767b856f1144732109262f44c803c67d634fd7f9b62d81919bfc210d8ae660b43ef820762489eba54b35a7a1799a1f9ee91eb8fdfc3df16000c
6
+ metadata.gz: f3519a72a83f31a146da97ffb2422439231afa6a00b55eb55f3ceb1f77d8766f1afb76fe60454bc403fccd7fe2689d2c76fe2970b2557b594258c28f834f478e
7
+ data.tar.gz: 5296734002aca4221526d0bbd98ff2927bb61b6b74781f46d2d79c5bf01d29d5d0a16bfe59aa0b313b9819fe823150324f247387a7ed310d6fe747e9f8cb4a02
data/CHANGELOG CHANGED
@@ -1,3 +1,14 @@
1
+ * Tue Feb 9 2016 - version 0.5.0
2
+ - Helper methods for testing APIs
3
+ - Added tests covering the basic functionality
4
+ - Fixed overflow in table of contents in online API doc
5
+ - Authentication chain can be empty
6
+ - Fixed Params.optional
7
+ - Fixed coercion of Float input parameters
8
+ - Renamed Params::Param to Parameters::Typed and Params::Resource to
9
+ Parameters::Resource
10
+ - Instance-level hooks are stored in the object itself
11
+
1
12
  * Sun Jan 24 2016 - version 0.4.2
2
13
  - Inclusion validator: handle hash correctly, don't overwrite given values
3
14
 
data/Rakefile CHANGED
@@ -7,6 +7,7 @@ require 'haveapi/tasks/yard'
7
7
 
8
8
  RSpec::Core::RakeTask.new(:spec) do |spec|
9
9
  spec.pattern = FileList['spec/**/*_spec.rb']
10
+ spec.rspec_opts = '--require spec_helper'
10
11
  end
11
12
 
12
13
  begin
data/haveapi.gemspec CHANGED
@@ -5,7 +5,7 @@ require 'haveapi/version'
5
5
  Gem::Specification.new do |s|
6
6
  s.name = 'haveapi'
7
7
  s.version = HaveAPI::VERSION
8
- s.date = '2016-01-24'
8
+ s.date = '2016-02-09'
9
9
  s.summary =
10
10
  s.description = 'Framework for creating self-describing APIs'
11
11
  s.authors = 'Jakub Skokan'
@@ -37,6 +37,8 @@ module HaveAPI::Authentication
37
37
  # Authentication provider can deny the user access by calling Base#deny.
38
38
  def authenticate(v, *args)
39
39
  catch(:return) do
40
+ return unless @instances[v]
41
+
40
42
  @instances[v].each do |provider|
41
43
  u = provider.authenticate(*args)
42
44
  return u if u
@@ -48,6 +50,8 @@ module HaveAPI::Authentication
48
50
 
49
51
  def describe(context)
50
52
  ret = {}
53
+
54
+ return ret unless @instances[context.version]
51
55
 
52
56
  @instances[context.version].each do |provider|
53
57
  ret[provider.name] = provider.describe
data/lib/haveapi/hooks.rb CHANGED
@@ -7,14 +7,36 @@ module HaveAPI
7
7
  # it is possible to connect to a specific instance and not for
8
8
  # all instances of a class.
9
9
  #
10
+ # Hook definition contains additional information for as a documentation:
11
+ # description, context, arguments, return value.
12
+ #
13
+ # Every hook can have multiple listeners. They are invoked in the order of
14
+ # registration. Instance-level listeners first, then class-level. Hooks are
15
+ # chained using the block's first argument and return value. The first block
16
+ # to be executed gets the initial value, may make changes and returns it.
17
+ # The next block gets the return value of the previous block as its first
18
+ # argument, may make changes and returns it. Return value of the last block
19
+ # is returned to the caller of the hook.
20
+ #
10
21
  # === \Usage
11
22
  # ==== \Register hooks
12
23
  # class MyClass
13
24
  # include Hookable
14
25
  #
15
- # has_hook :myhook
26
+ # has_hook :myhook,
27
+ # desc: 'Called when I want to',
28
+ # context: 'current',
29
+ # args: {
30
+ # a: 'integer',
31
+ # b: 'integer',
32
+ # c: 'integer',
33
+ # }
16
34
  # end
17
35
  #
36
+ # Not that the additional information is just optional. A list of defined
37
+ # hooks and their description is a part of the reference documentation
38
+ # generated by yard.
39
+ #
18
40
  # ==== \Class level hooks
19
41
  # # Connect hook
20
42
  # MyClass.connect_hook(:myhook) do |ret, a, b, c|
@@ -43,7 +65,20 @@ module HaveAPI
43
65
  # my.call_class_hooks_for(:myhook, args: [1, 2, 3])
44
66
  # # Call both instance and class hooks at once
45
67
  # my.call_hooks_for(:myhook, args: [1, 2, 3])
68
+ #
69
+ # ==== \Chaining
70
+ # 5.times do |i|
71
+ # MyClass.connect_hook(:myhook) do |ret, a, b, c|
72
+ # ret[:counter] += i
73
+ # ret
74
+ # end
75
+ # end
76
+ #
77
+ # p MyClass.call_hooks(:myhook, args: [1, 2, 3], initial: {counter: 0})
78
+ # => {:counter=>5}
46
79
  module Hooks
80
+ INSTANCE_VARIABLE = '@_haveapi_hooks'
81
+
47
82
  # Register a hook defined by +klass+ with +name+.
48
83
  # +klass+ is an instance of Class, that is class name, not it's instance.
49
84
  # +opts+ is a hash and can have following keys:
@@ -72,16 +107,16 @@ module HaveAPI
72
107
  end
73
108
 
74
109
  # Connect instance hook from instance +klass+ with +name+ to +block+.
75
- def self.connect_instance_hook(klass, name, &block)
76
- unless @hooks[klass]
77
- @hooks[klass] = {}
110
+ def self.connect_instance_hook(instance, name, &block)
111
+ hooks = instance.instance_variable_get(INSTANCE_VARIABLE)
78
112
 
79
- @hooks[klass.class].each do |k, v|
80
- @hooks[klass][k] = {listeners: []}
81
- end
113
+ unless hooks
114
+ hooks = {}
115
+ instance.instance_variable_set(INSTANCE_VARIABLE, hooks)
82
116
  end
83
117
 
84
- @hooks[klass][name][:listeners] << block
118
+ hooks[name] ||= {listeners: []}
119
+ hooks[name][:listeners] << block
85
120
  end
86
121
 
87
122
  # Call all blocks that are connected to hook in +klass+ with +name+.
@@ -98,17 +133,39 @@ module HaveAPI
98
133
  # A block may decide that no further blocks should be executed.
99
134
  # In such a case it calls Hooks.stop with the return value. It is then
100
135
  # returned to the caller immediately.
101
- def self.call_for(klass, name, where = nil, args: [], initial: {})
136
+ #
137
+ # @param klass [Class instance, instance]
138
+ # @param name [Symbol] hook name
139
+ # @param where [Class instance] class in whose context hooks are executed
140
+ # @param args [Array] an array of arguments passed to hooks
141
+ # @param initial [Hash] initial return value
142
+ # @param instance [Boolean] call instance hooks or not; nil means auto-detect
143
+ def self.call_for(
144
+ klass,
145
+ name,
146
+ where = nil,
147
+ args: [],
148
+ initial: {},
149
+ instance: nil
150
+ )
102
151
  classified = hook_classify(klass)
103
152
 
153
+ if (instance.nil? && !classified.is_a?(Class)) || instance
154
+ all_hooks = klass.instance_variable_get(INSTANCE_VARIABLE)
155
+
156
+ else
157
+ all_hooks = @hooks[classified]
158
+ end
159
+
104
160
  catch(:stop) do
105
- return initial unless @hooks[classified]
106
- hooks = @hooks[classified][name][:listeners]
161
+ return initial unless all_hooks
162
+ hooks = all_hooks[name][:listeners]
107
163
  return initial unless hooks
108
164
 
109
165
  hooks.each do |hook|
110
166
  if where
111
167
  ret = where.instance_exec(initial, *args, &hook)
168
+
112
169
  else
113
170
  ret = hook.call(initial, *args)
114
171
  end
@@ -362,7 +362,7 @@ END
362
362
 
363
363
  def validator_for(param, key, opts)
364
364
  @params.each do |p|
365
- next unless p.is_a?(::HaveAPI::Parameters::Param)
365
+ next unless p.is_a?(::HaveAPI::Parameters::Typed)
366
366
 
367
367
  if p.db_name == param
368
368
  p.add_validator(key, opts)
@@ -1,5 +1,5 @@
1
1
  module HaveAPI::Parameters
2
- class Param
2
+ class Typed
3
3
  ATTRIBUTES = %i(label desc type db_name default fill clean)
4
4
 
5
5
  attr_reader :name, :label, :desc, :type, :default
@@ -76,6 +76,9 @@ module HaveAPI::Parameters
76
76
 
77
77
  elsif @type == Integer
78
78
  raw.to_i
79
+
80
+ elsif @type == Float
81
+ raw.to_f
79
82
 
80
83
  elsif @type == Boolean
81
84
  Boolean.to_b(raw)
@@ -83,7 +83,7 @@ module HaveAPI
83
83
  end
84
84
 
85
85
  def optional(*args)
86
- add_param(*apply(args, required: true))
86
+ add_param(*apply(args, required: false))
87
87
  end
88
88
 
89
89
  def string(*args)
@@ -268,7 +268,7 @@ module HaveAPI
268
268
 
269
269
  private
270
270
  def add_param(*args)
271
- p = Parameters::Param.new(*args)
271
+ p = Parameters::Typed.new(*args)
272
272
 
273
273
  return if @include && !@include.include?(p.name)
274
274
  return if @exclude && @exclude.include?(p.name)
@@ -0,0 +1,75 @@
1
+ module HaveAPI::Spec
2
+ # Contains methods for specification of API to be used in `description` block.
3
+ module ApiBuilder
4
+ # Uses an empty module as the source for the API. It will have no resources,
5
+ # no actions. Version default to `1`.
6
+ def empty_api
7
+ api(Module.new)
8
+ default_version(1)
9
+ end
10
+
11
+ # Set an API module or create the API using DSL.
12
+ # @param mod [Module] module name or nil
13
+ # @yield block is executed in a dynamically created module
14
+ def api(mod = nil, &block)
15
+ unless mod
16
+ mod = Module.new do
17
+ def self.define_resource(name, superclass: Resource, &block)
18
+ return false if const_defined?(name)
19
+
20
+ cls = Class.new(superclass)
21
+ const_set(name, cls)
22
+ cls.class_exec(&block) if block
23
+ cls
24
+ end
25
+
26
+ module_eval(&block)
27
+ end
28
+
29
+ const_set(:ApiModule, mod)
30
+ end
31
+
32
+ opt(:api_module, mod)
33
+ end
34
+
35
+ # Set authentication chain.
36
+ def auth_chain(chain)
37
+ opt(:auth_chain, chain)
38
+ end
39
+
40
+ # Select API versions to be used.
41
+ def use_version(v)
42
+ before(:each) do
43
+ opts(:versions, v)
44
+ end
45
+ end
46
+
47
+ # Set default API version.
48
+ def default_version(v)
49
+ opt(:default_version, v)
50
+ end
51
+
52
+ # Set a custom mount path.
53
+ def mount_to(path)
54
+ opts(:mount, path)
55
+ end
56
+
57
+ # Login using HTTP basic.
58
+ def login(*credentials)
59
+ before(:each) do
60
+ basic_authorize(*credentials)
61
+ end
62
+ end
63
+
64
+ # @private
65
+ def opts
66
+ @opts
67
+ end
68
+
69
+ # @private
70
+ def opt(name, v)
71
+ @opts ||= {}
72
+ @opts[name] = v
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,41 @@
1
+ module HaveAPI::Spec
2
+ # This class wraps raw reply from the API and provides a more friendly
3
+ # interface.
4
+ class ApiResponse
5
+ def initialize(body)
6
+ @data = JSON.parse(body, symbolize_names: true)
7
+ end
8
+
9
+ def envelope
10
+ @data
11
+ end
12
+
13
+ def status
14
+ @data[:status]
15
+ end
16
+
17
+ def ok?
18
+ @data[:status]
19
+ end
20
+
21
+ def failed?
22
+ !ok?
23
+ end
24
+
25
+ def response
26
+ @data[:response]
27
+ end
28
+
29
+ def message
30
+ @data[:message]
31
+ end
32
+
33
+ def errors
34
+ @data[:errors]
35
+ end
36
+
37
+ def [](k)
38
+ @data[:response][k]
39
+ end
40
+ end
41
+ end
@@ -1,103 +1,9 @@
1
1
  require 'rack/test'
2
2
 
3
3
  module HaveAPI
4
- # Contains methods for specification of API to be used in +description+ block.
5
- module ApiBuilder
6
- def auth_chain(chain)
7
- @auth_chain = chain
8
- end
9
-
10
- def use_version(v)
11
- before(:each) do
12
- @versions = v
13
- end
14
- end
15
-
16
- def default_version(v)
17
- @default_version = v
18
- end
19
-
20
- def mount_to(path)
21
- @mount = path
22
- end
23
-
24
- def login(*credentials)
25
- @username, @password = credentials
26
-
27
- before(:each) do
28
- basic_authorize(*credentials)
29
- end
30
- end
31
- end
32
-
33
- # Helper methods for specs.
34
- module SpecMethods
35
- include Rack::Test::Methods
36
-
37
- # This class wraps raw reply from the API and provides more friendly
38
- # interface.
39
- class ApiResponse
40
- def initialize(body)
41
- @data = JSON.parse(body, symbolize_names: true)
42
- end
43
-
44
- def status
45
- @data[:status]
46
- end
47
-
48
- def ok?
49
- @data[:status]
50
- end
51
-
52
- def failed?
53
- !ok?
54
- end
55
-
56
- def response
57
- @data[:response]
58
- end
59
-
60
- def message
61
- @data[:message]
62
- end
63
-
64
- def errors
65
- @data[:errors]
66
- end
67
-
68
- def [](k)
69
- @data[:response][k]
70
- end
71
- end
72
-
73
- def app
74
- api = HaveAPI::Server.new
75
- api.auth_chain << @auth_chain if @auth_chain
76
- api.use_version(@versions || :all)
77
- api.set_default_version(@default_version) if @default_version
78
- api.mount(@mount || '/')
79
- api.app
80
- end
81
-
82
- # Login with HTTP basic auth.
83
- def login(*credentials)
84
- basic_authorize(*credentials)
85
- end
86
-
87
- # Make API request.
88
- # This method is a wrapper for Rack::Test::Methods. Input parameters
89
- # are encoded into JSON and sent with correct Content-Type.
90
- def api(http_method, url, params={})
91
- method(http_method).call(
92
- url,
93
- params.to_json,
94
- {'Content-Type' => 'application/json'}
95
- )
96
- end
97
-
98
- # Return parsed API response.
99
- def api_response
100
- @api_response ||= ApiResponse.new(last_response.body)
101
- end
102
- end
4
+ module Spec ; end
103
5
  end
6
+
7
+ require_relative 'api_builder'
8
+ require_relative 'api_response'
9
+ require_relative 'spec_methods'
@@ -0,0 +1,32 @@
1
+ module HaveAPI::Spec
2
+ class MockAction
3
+ def initialize(test, server, action, url, v)
4
+ @test = test
5
+ @server = server
6
+ @action = action
7
+ @url = url
8
+ @v = v
9
+ end
10
+
11
+ def call(input, user: nil, &block)
12
+ action = @action.new(nil, @v, input, nil, HaveAPI::Context.new(
13
+ @server,
14
+ version: @v,
15
+ action: @action,
16
+ url: @url,
17
+ params: input,
18
+ user: user,
19
+ endpoint: true
20
+ ))
21
+
22
+ unless action.authorized?(user)
23
+ fail 'Access denied. Insufficient permissions.'
24
+ end
25
+
26
+ status, data, errors = action.safe_exec
27
+ fail (data || 'action failed') unless status
28
+ action.instance_exec(@test, &block)
29
+ data
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,121 @@
1
+ module HaveAPI::Spec
2
+ # Helper methods for specs.
3
+ module SpecMethods
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ return @api.app if @api
8
+
9
+ auth = get_opt(:auth_chain)
10
+ default = get_opt(:default_version)
11
+
12
+ @api = HaveAPI::Server.new(get_opt(:api_module))
13
+ @api.auth_chain << auth if auth
14
+ @api.use_version(get_opt(:versions) || :all)
15
+ @api.default_version = default if default
16
+ @api.mount(get_opt(:mount) || '/')
17
+ @api.app
18
+ end
19
+
20
+ # Login with HTTP basic auth.
21
+ def login(*credentials)
22
+ basic_authorize(*credentials)
23
+ end
24
+
25
+ # Make API request.
26
+ # This method is a wrapper for Rack::Test::Methods. Input parameters
27
+ # are encoded into JSON and sent with a correct Content-Type.
28
+ # Two modes:
29
+ # http_method, url, params = {}
30
+ # [resource], action, params, &block
31
+ def call_api(*args, &block)
32
+ if args[0].is_a?(::Array) || args[1].is_a?(::Symbol)
33
+ r_name, a_name, params = args
34
+
35
+ app
36
+
37
+ action, url = find_action(
38
+ (params && params[:version]) || @api.default_version,
39
+ r_name, a_name
40
+ )
41
+
42
+ method(action.http_method).call(
43
+ url,
44
+ params && params.to_json,
45
+ {'Content-Type' => 'application/json'}
46
+ )
47
+
48
+ else
49
+ http_method, url, params = args
50
+
51
+ method(http_method).call(
52
+ url,
53
+ params && params.to_json,
54
+ {'Content-Type' => 'application/json'}
55
+ )
56
+ end
57
+ end
58
+
59
+ # Mock action call. Note that this method does not involve rack request/response
60
+ # in any way. It simply creates an instance of specified action and executes it.
61
+ # Provided block is executed in the context of the action instance after `exec()`
62
+ # has been called.
63
+ #
64
+ # If `exec()` signals error, the block is not called at all, but `RuntimeError`
65
+ # is raised instead.
66
+ #
67
+ # Authentication does not take place. Argument `user` may be used to provide
68
+ # user object. That will signify that the user is authenticated and it will be passed
69
+ # to Action.authorize.
70
+ #
71
+ # @param r_name [Array, Symbol] path to resource in the API
72
+ # @param a_name [Symbol] name of wanted action
73
+ # @param params [Hash] a hash of parameters, must contain correct namespace
74
+ # @param version [any] API version, if not specified, the default version is used
75
+ # @param user [any] object representing authenticated user
76
+ # @yield [self] the block is executed in the action instance
77
+ def mock_action(r_name, a_name, params, version: nil, user: nil, &block)
78
+ app
79
+ v = version || @api.default_version
80
+ action, url = find_action(v, r_name, a_name)
81
+ m = MockAction.new(self, @api, action, url, v)
82
+ m.call(params, user: user, &block)
83
+ end
84
+
85
+ # Return parsed API response.
86
+ # @return [HaveAPI::Spec::ApiResponse]
87
+ def api_response
88
+ if last_response != @last_response
89
+ @last_response = last_response
90
+ @api_response = ApiResponse.new(last_response.body)
91
+
92
+ else
93
+ @api_response ||= ApiResponse.new(last_response.body)
94
+ end
95
+ end
96
+
97
+ protected
98
+ def get_opt(name)
99
+ self.class.opts && self.class.opts[name]
100
+ end
101
+
102
+ def find_action(v, r_name, a_name)
103
+ # Make sure the API is built
104
+ app
105
+
106
+ resources = r_name.is_a?(::Array) ? r_name : [r_name]
107
+
108
+ top = @api.routes[v]
109
+
110
+ resources.each do |r|
111
+ top = top[:resources].detect do |k, _|
112
+ k.resource_name.to_sym == r
113
+ end.second
114
+ end
115
+
116
+ top[:actions].detect do |k, v|
117
+ k.to_s.demodulize.underscore.to_sym == a_name
118
+ end
119
+ end
120
+ end
121
+ end
@@ -1,4 +1,4 @@
1
1
  module HaveAPI
2
2
  PROTOCOL_VERSION = '1.0'
3
- VERSION = '0.4.2'
3
+ VERSION = '0.5.0'
4
4
  end
@@ -20,7 +20,9 @@
20
20
  }
21
21
  .affix {
22
22
  width: inherit;
23
- position: fixed;
23
+ height: 100%;
24
+ position: fixed;
25
+ overflow-y: auto;
24
26
  }
25
27
  .affix-bottom {
26
28
  width: inherit;
data/lib/haveapi.rb CHANGED
@@ -13,7 +13,7 @@ module HaveAPI
13
13
  end
14
14
 
15
15
  require_relative 'haveapi/params'
16
- require_rel 'haveapi/params/'
16
+ require_rel 'haveapi/parameters/'
17
17
  require_rel 'haveapi/*.rb'
18
18
  require_rel 'haveapi/model_adapters/hash'
19
19
  require_rel 'haveapi/model_adapters/active_record' if ar