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
@@ -0,0 +1,199 @@
1
+ describe HaveAPI::Action do
2
+ context 'DSL' do
3
+ it 'inherits input' do
4
+ class Resource < HaveAPI::Resource
5
+ class InputAction < HaveAPI::Action
6
+ input do
7
+ string :param
8
+ end
9
+ end
10
+
11
+ class SubInputAction < InputAction ; end
12
+ end
13
+
14
+ # Invokes execution of input/output blocks
15
+ Resource.routes
16
+ expect(Resource::SubInputAction.input.params.first.name).to eq(:param)
17
+ end
18
+
19
+ it 'inherits output' do
20
+ class Resource < HaveAPI::Resource
21
+ class OutputAction < HaveAPI::Action
22
+ output do
23
+ string :param
24
+ end
25
+ end
26
+
27
+ class SubOutputAction < OutputAction ; end
28
+ end
29
+
30
+ # Invokes execution of input/output blocks
31
+ Resource.routes
32
+ expect(Resource::SubOutputAction.output.params.first.name).to eq(:param)
33
+ end
34
+
35
+ it 'chains input' do
36
+ class Resource < HaveAPI::Resource
37
+ class InputChainAction < HaveAPI::Action
38
+ input do
39
+ string :param1
40
+ end
41
+
42
+ input do
43
+ string :param2
44
+ end
45
+ end
46
+ end
47
+
48
+ # Invokes execution of input/output blocks
49
+ Resource.routes
50
+
51
+ params = Resource::InputChainAction.input.params.map { |p| p.name }
52
+ expect(params).to contain_exactly(:param1, :param2)
53
+ end
54
+
55
+ it 'chains output' do
56
+ class Resource < HaveAPI::Resource
57
+ class OutputChainAction < HaveAPI::Action
58
+ output do
59
+ string :param1
60
+ end
61
+
62
+ output do
63
+ string :param2
64
+ end
65
+ end
66
+ end
67
+
68
+ # Invokes execution of input/output blocks
69
+ Resource.routes
70
+
71
+ params = Resource::OutputChainAction.output.params.map { |p| p.name }
72
+ expect(params).to contain_exactly(:param1, :param2)
73
+ end
74
+
75
+ it 'can combine chaining and inheritance' do
76
+ class Resource < HaveAPI::Resource
77
+ class BaseAction < HaveAPI::Action
78
+ input do
79
+ string :inbase1
80
+ end
81
+
82
+ input do
83
+ string :inbase2
84
+ end
85
+
86
+ output do
87
+ string :outbase1
88
+ end
89
+
90
+ output do
91
+ string :outbase2
92
+ end
93
+ end
94
+
95
+ class SubAction < BaseAction
96
+ input do
97
+ string :insub1
98
+ string :insub2
99
+ end
100
+
101
+ input do
102
+ string :insub3
103
+ end
104
+
105
+ output do
106
+ string :outsub1
107
+ string :outsub2
108
+ end
109
+
110
+ output do
111
+ string :outsub3
112
+ end
113
+ end
114
+ end
115
+
116
+ # Invokes execution of input/output blocks
117
+ Resource.routes
118
+
119
+ input = Resource::SubAction.input.params.map { |p| p.name }
120
+ output = Resource::SubAction.output.params.map { |p| p.name }
121
+
122
+ expect(input).to contain_exactly(*%i(inbase1 inbase2 insub1 insub2 insub3))
123
+ expect(output).to contain_exactly(*%i(outbase1 outbase2 outsub1 outsub2 outsub3))
124
+ end
125
+
126
+ it 'sets layout' do
127
+ class Resource < HaveAPI::Resource
128
+ class DefaultLayoutAction < HaveAPI::Action ; end
129
+
130
+ class ObjectLayoutAction < HaveAPI::Action
131
+ input(:object) {}
132
+ output(:object) {}
133
+ end
134
+
135
+ class ObjectListLayoutAction < HaveAPI::Action
136
+ input(:object_list) {}
137
+ output(:object_list) {}
138
+ end
139
+
140
+ class HashLayoutAction < HaveAPI::Action
141
+ input(:hash) {}
142
+ output(:hash) {}
143
+ end
144
+
145
+ class HashListLayoutAction < HaveAPI::Action
146
+ input(:hash_list) {}
147
+ output(:hash_list) {}
148
+ end
149
+
150
+ class CombinedLayoutAction < HaveAPI::Action
151
+ input(:hash) {}
152
+ output(:object_list) {}
153
+ end
154
+ end
155
+
156
+ expect(Resource::DefaultLayoutAction.input.layout).to eq(:object)
157
+ expect(Resource::DefaultLayoutAction.output.layout).to eq(:object)
158
+
159
+ expect(Resource::ObjectLayoutAction.input.layout).to eq(:object)
160
+ expect(Resource::ObjectLayoutAction.output.layout).to eq(:object)
161
+
162
+ expect(Resource::ObjectListLayoutAction.input.layout).to eq(:object_list)
163
+ expect(Resource::ObjectListLayoutAction.output.layout).to eq(:object_list)
164
+
165
+ expect(Resource::HashLayoutAction.input.layout).to eq(:hash)
166
+ expect(Resource::HashLayoutAction.output.layout).to eq(:hash)
167
+
168
+ expect(Resource::HashListLayoutAction.input.layout).to eq(:hash_list)
169
+ expect(Resource::HashListLayoutAction.output.layout).to eq(:hash_list)
170
+
171
+ expect(Resource::CombinedLayoutAction.input.layout).to eq(:hash)
172
+ expect(Resource::CombinedLayoutAction.output.layout).to eq(:object_list)
173
+ end
174
+
175
+ it 'catches exceptions in input' do
176
+ class ExResourceIn < HaveAPI::Resource
177
+ class ExInputAction < HaveAPI::Action
178
+ input do
179
+ fail 'this is terrible!'
180
+ end
181
+ end
182
+ end
183
+
184
+ expect { ExResourceIn.routes }.to raise_error(HaveAPI::BuildError)
185
+ end
186
+
187
+ it 'catches exceptions in output' do
188
+ class ExResourceOut < HaveAPI::Resource
189
+ class ExOutputAction < HaveAPI::Action
190
+ output do
191
+ fail 'this is terrible!'
192
+ end
193
+ end
194
+ end
195
+
196
+ expect { ExResourceOut.routes }.to raise_error(HaveAPI::BuildError)
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,113 @@
1
+ describe HaveAPI::Authorization do
2
+ class Resource < HaveAPI::Resource
3
+ class Index < HaveAPI::Actions::Default::Index
4
+ input do
5
+ string :param1
6
+ string :param2
7
+ end
8
+
9
+ output do
10
+ string :param1
11
+ string :param2
12
+ end
13
+ end
14
+
15
+ routes
16
+ end
17
+
18
+ it 'defaults to deny' do
19
+ auth = HaveAPI::Authorization.new {}
20
+ expect(auth.authorized?(nil)).to be false
21
+ end
22
+
23
+ it 'can authorize' do
24
+ auth = HaveAPI::Authorization.new { allow }
25
+ expect(auth.authorized?(nil)).to be true
26
+ end
27
+
28
+ it 'applies restrictions' do
29
+ auth = HaveAPI::Authorization.new do
30
+ restrict filter: true
31
+ allow
32
+ end
33
+
34
+ expect(auth.authorized?(nil)).to be true
35
+ expect(auth.restrictions[:filter]).to be true
36
+ end
37
+
38
+ it 'whitelists input' do
39
+ auth = HaveAPI::Authorization.new do
40
+ input whitelist: %i(param1)
41
+ allow
42
+ end
43
+
44
+ expect(auth.authorized?(nil)).to be true
45
+
46
+ action = Resource::Index
47
+
48
+ expect(auth.filter_input(
49
+ action.input.params,
50
+ action.model_adapter(action.input.layout).input({
51
+ param1: '123',
52
+ param2: '456',
53
+ })
54
+ ).keys).to contain_exactly(:param1)
55
+ end
56
+
57
+ it 'blacklists input' do
58
+ auth = HaveAPI::Authorization.new do
59
+ input blacklist: %i(param1)
60
+ allow
61
+ end
62
+
63
+ expect(auth.authorized?(nil)).to be true
64
+
65
+ action = Resource::Index
66
+
67
+ expect(auth.filter_input(
68
+ action.input.params,
69
+ action.model_adapter(action.input.layout).input({
70
+ param1: '123',
71
+ param2: '456',
72
+ })
73
+ ).keys).to contain_exactly(:param2)
74
+ end
75
+
76
+ it 'whitelists output' do
77
+ auth = HaveAPI::Authorization.new do
78
+ output whitelist: %i(param1)
79
+ allow
80
+ end
81
+
82
+ expect(auth.authorized?(nil)).to be true
83
+
84
+ action = Resource::Index
85
+
86
+ expect(auth.filter_output(
87
+ action.output.params,
88
+ action.model_adapter(action.output.layout).output(nil, {
89
+ param1: '123',
90
+ param2: '456',
91
+ })
92
+ ).keys).to contain_exactly(:param1)
93
+ end
94
+
95
+ it 'blacklists output' do
96
+ auth = HaveAPI::Authorization.new do
97
+ output blacklist: %i(param1)
98
+ allow
99
+ end
100
+
101
+ expect(auth.authorized?(nil)).to be true
102
+
103
+ action = Resource::Index
104
+
105
+ expect(auth.filter_output(
106
+ action.output.params,
107
+ action.model_adapter(action.output.layout).output(nil, {
108
+ param1: '123',
109
+ param2: '456',
110
+ })
111
+ ).keys).to contain_exactly(:param2)
112
+ end
113
+ end
@@ -0,0 +1,47 @@
1
+ describe HaveAPI::Common do
2
+ class Test1 < HaveAPI::Common
3
+ has_attr :attr1
4
+ has_attr :attr2, 42
5
+ end
6
+
7
+ it 'defines attributes' do
8
+ expect(Test1.attr1).to be_nil
9
+ expect(Test1.attr2).to eq(42)
10
+ end
11
+
12
+ class Test2 < HaveAPI::Common
13
+ has_attr :attr1
14
+ has_attr :attr2, 42
15
+ end
16
+
17
+ it 'sets attributes' do
18
+ Test2.attr1 'val1'
19
+ Test2.attr2 663
20
+
21
+ expect(Test2.attr1).to eq('val1')
22
+ expect(Test2.attr2).to eq(663)
23
+ end
24
+
25
+ class Test3 < HaveAPI::Common
26
+ has_attr :attr1
27
+ has_attr :attr2, 42
28
+ has_attr :attr3
29
+
30
+ attr1 'foo'
31
+ attr2 :bar
32
+
33
+ def self.inherited(subclass)
34
+ inherit_attrs(subclass)
35
+ end
36
+ end
37
+
38
+ class SubTest3 < Test3
39
+ attr3 'bar'
40
+ end
41
+
42
+ it 'inherites attributes' do
43
+ expect(SubTest3.attr1).to eq('foo')
44
+ expect(SubTest3.attr2).to eq(:bar)
45
+ expect(SubTest3.attr3).to eq('bar')
46
+ end
47
+ end
@@ -0,0 +1,29 @@
1
+ describe 'Documentation' do
2
+ it 'responds to OPTIONS /' do
3
+
4
+ end
5
+
6
+ it 'responds to OPTIONS /?describe=versions' do
7
+
8
+ end
9
+
10
+ it 'responds to OPTIONS /?describe=default' do
11
+
12
+ end
13
+
14
+ it 'responds to OPTIONS /<version>' do
15
+
16
+ end
17
+
18
+ it 'responds to OPTIONS /<every action>?method=<method>' do
19
+
20
+ end
21
+
22
+ it 'has online doc' do
23
+
24
+ end
25
+
26
+ it 'has online doc for every version' do
27
+
28
+ end
29
+ end
@@ -0,0 +1,33 @@
1
+ describe 'Envelope' do
2
+ context 'documentation' do
3
+ empty_api
4
+
5
+ it 'returns correct envelope' do
6
+ call_api(:options, '/')
7
+ expect(api_response.envelope.keys).to contain_exactly(
8
+ *%i(version status response message errors)
9
+ )
10
+ end
11
+
12
+ it 'succeeds' do
13
+ call_api(:options, '/')
14
+ expect(api_response).to be_ok
15
+ end
16
+ end
17
+
18
+ context 'data' do
19
+ empty_api
20
+
21
+ it 'returns correct envelope' do
22
+ call_api(:get, '/unknown_resource')
23
+ expect(api_response.envelope.keys).to contain_exactly(
24
+ *%i(status response message errors)
25
+ )
26
+ end
27
+
28
+ it 'fails' do
29
+ call_api(:get, '/unknown_resource')
30
+ expect(api_response).to_not be_ok
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,146 @@
1
+ describe HaveAPI::Hooks do
2
+ class ClassLevel
3
+ include HaveAPI::Hookable
4
+
5
+ has_hook :simple_hook
6
+ has_hook :arg_hook
7
+ has_hook :ret_hook
8
+ has_hook :context_hook
9
+ end
10
+
11
+ class InstanceLevel
12
+ include HaveAPI::Hookable
13
+
14
+ has_hook :simple_hook
15
+ has_hook :arg_hook
16
+ has_hook :ret_hook
17
+ has_hook :context_hook
18
+ end
19
+
20
+ def connect_hook(name, &block)
21
+ @obj.connect_hook(name, &block)
22
+ end
23
+
24
+ def call_hooks(*args)
25
+ case @level
26
+ when :class
27
+ @obj.call_hooks(*args)
28
+
29
+ when :instance
30
+ if @method
31
+ @obj.method(@method).call(*args)
32
+
33
+ else
34
+ @obj.call_hooks_for(*args)
35
+ end
36
+
37
+ else
38
+ fail "unknown level '#{@level}'"
39
+ end
40
+ end
41
+
42
+ shared_examples(:common) do
43
+ it 'calls hooks' do
44
+ called = false
45
+
46
+ connect_hook(:simple_hook) do |ret|
47
+ called = true
48
+ ret
49
+ end
50
+
51
+ call_hooks(:simple_hook)
52
+ expect(called).to be true
53
+ end
54
+
55
+ it 'passes arguments' do
56
+ called = false
57
+
58
+ connect_hook(:arg_hook) do |ret, a, b, c|
59
+ called = true
60
+ expect([a, b, c]).to eq([1, 2, 3])
61
+ ret
62
+ end
63
+
64
+ call_hooks(:arg_hook, args: [1, 2, 3])
65
+ expect(called).to be true
66
+ end
67
+
68
+ it 'chains hooks' do
69
+ arr = []
70
+
71
+ 5.times do |i|
72
+ connect_hook(:simple_hook) do |ret|
73
+ arr << i
74
+ ret
75
+ end
76
+ end
77
+
78
+ call_hooks(:simple_hook)
79
+ expect(arr).to eq([0, 1, 2, 3, 4])
80
+ end
81
+
82
+ it 'chains return value' do
83
+ 5.times do
84
+ connect_hook(:ret_hook) do |ret|
85
+ ret[:counter] += 1
86
+ ret
87
+ end
88
+ end
89
+
90
+ sum = call_hooks(:ret_hook, initial: {counter: 0})
91
+ expect(sum[:counter]).to eq(5)
92
+ end
93
+
94
+ it 'executes block in given context' do
95
+ class CustomEnv
96
+ def foo
97
+ 'bar'
98
+ end
99
+ end
100
+
101
+ connect_hook(:context_hook) do |ret|
102
+ ret[:val] = foo
103
+ ret
104
+ end
105
+
106
+ res = call_hooks(:context_hook, CustomEnv.new)
107
+ expect(res[:val]).to eq('bar')
108
+ end
109
+ end
110
+
111
+ context 'on class level' do
112
+ before(:each) do
113
+ @obj = ClassLevel
114
+ @level = :class
115
+ end
116
+
117
+ include_examples :common
118
+ end
119
+
120
+ context 'on instance level' do
121
+ context 'all hooks' do
122
+ before(:each) do
123
+ @obj = InstanceLevel.new
124
+ @level = :instance
125
+ end
126
+
127
+ include_examples :common
128
+ end
129
+
130
+ context 'only class hooks' do
131
+ # FIXME: class hooks fail (that is correct, no class hooks are defined)
132
+ # must find a way to test failure
133
+ #include_examples :common
134
+ end
135
+
136
+ context 'only instance hooks' do
137
+ before(:each) do
138
+ @obj = InstanceLevel.new
139
+ @level = :instance
140
+ @method = :call_instance_hooks_for
141
+ end
142
+
143
+ include_examples :common
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,93 @@
1
+ require 'time'
2
+
3
+ describe 'Parameters::Typed' do
4
+ def p_type(type)
5
+ HaveAPI::Parameters::Typed.new(:param1, type: type)
6
+ end
7
+
8
+ def p_arg(arg = {})
9
+ HaveAPI::Parameters::Typed.new(*[:param1, arg])
10
+ end
11
+
12
+ it 'does not change provided arguments' do
13
+ kwargs = {
14
+ label: 'Param 1',
15
+ desc: 'Desc',
16
+ required: true,
17
+ }
18
+ p_arg(kwargs)
19
+ expect(kwargs.keys).to contain_exactly(*%i(label desc required))
20
+ end
21
+
22
+ it 'automatically sets label' do
23
+ p = p_arg
24
+ expect(p.label).to eq('Param1')
25
+ end
26
+
27
+ it 'accepts custom label' do
28
+ p = p_arg(label: 'Custom')
29
+ expect(p.label).to eq('Custom')
30
+ end
31
+
32
+ it 'rejects unknown parameters' do
33
+ expect do
34
+ p_arg(shiny: true)
35
+ end.to raise_error(RuntimeError)
36
+ end
37
+
38
+ it 'can be required' do
39
+ p = p_arg(required: true)
40
+ expect(p.required?).to be true
41
+ end
42
+
43
+ it 'can be optional' do
44
+ p = p_arg
45
+ expect(p.optional?).to be true
46
+
47
+ p = p_arg(required: false)
48
+ expect(p.optional?).to be true
49
+
50
+ p = p_arg(required: nil)
51
+ expect(p.optional?).to be true
52
+ end
53
+
54
+ it 'cleans input value' do
55
+ # Integer
56
+ p = p_type(Integer)
57
+ expect(p.clean('42')).to eq(42)
58
+
59
+ # Float
60
+ p = p_type(Float)
61
+ expect(p.clean('3.1456')).to eq(3.1456)
62
+
63
+ # Boolean
64
+ p = p_type(Boolean)
65
+
66
+ %w(true t yes y 1).each do |v|
67
+ expect(p.clean(v)).to be true
68
+ end
69
+
70
+ %w(false f no n 0).each do |v|
71
+ expect(p.clean(v)).to be false
72
+ end
73
+
74
+ # Datetime
75
+ p = p_type(Datetime)
76
+ t = Time.now
77
+ t2 = Time.iso8601(t.iso8601)
78
+
79
+ expect(p.clean(t.iso8601)).to eq(t2)
80
+ expect { p.clean('bzz') }.to raise_error(HaveAPI::ValidationError)
81
+
82
+ # String, Text
83
+ p = p_type(String)
84
+ expect(p.clean('bzz')).to eq('bzz')
85
+
86
+ # Defaults
87
+ p = p_type(String)
88
+ expect(p.clean(nil)).to be_nil
89
+
90
+ p.patch(default: 'bazinga')
91
+ expect(p.clean(nil)).to eq('bazinga')
92
+ end
93
+ end