haveapi 0.4.2 → 0.5.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 (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