resource_set 1.0.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 +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +12 -0
  5. data/CHANGELOG.md +11 -0
  6. data/Gemfile +2 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +165 -0
  9. data/Rakefile +5 -0
  10. data/examples/digitalocean_droplets.rb +60 -0
  11. data/examples/httpbin_client.rb +15 -0
  12. data/lib/resource_set/action.rb +66 -0
  13. data/lib/resource_set/action_invoker.rb +58 -0
  14. data/lib/resource_set/endpoint_resolver.rb +46 -0
  15. data/lib/resource_set/inheritable_attribute.rb +20 -0
  16. data/lib/resource_set/method_factory.rb +20 -0
  17. data/lib/resource_set/resource.rb +40 -0
  18. data/lib/resource_set/resource_collection.rb +55 -0
  19. data/lib/resource_set/status_code_mapper.rb +59 -0
  20. data/lib/resource_set/testing/action_handler_matchers.rb +42 -0
  21. data/lib/resource_set/testing/have_action_matchers.rb +85 -0
  22. data/lib/resource_set/testing.rb +20 -0
  23. data/lib/resource_set/version.rb +3 -0
  24. data/lib/resource_set.rb +17 -0
  25. data/resource_set.gemspec +30 -0
  26. data/spec/integration/resource_actions_spec.rb +41 -0
  27. data/spec/lib/resource_set/action_invoker_spec.rb +167 -0
  28. data/spec/lib/resource_set/action_spec.rb +87 -0
  29. data/spec/lib/resource_set/endpoint_resolver_spec.rb +60 -0
  30. data/spec/lib/resource_set/inheritable_attribute_spec.rb +54 -0
  31. data/spec/lib/resource_set/method_factory_spec.rb +50 -0
  32. data/spec/lib/resource_set/resource_collection_spec.rb +67 -0
  33. data/spec/lib/resource_set/resource_spec.rb +66 -0
  34. data/spec/lib/resource_set/status_code_mapper_spec.rb +9 -0
  35. data/spec/lib/resource_set/testing/action_handler_matchers_spec.rb +68 -0
  36. data/spec/lib/resource_set/testing/have_action_matchers_spec.rb +157 -0
  37. data/spec/spec_helper.rb +8 -0
  38. metadata +202 -0
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ class DummyResourceActions < ResourceSet::Resource
4
+ resources do
5
+ action :dummy, 'GET /dummy' do
6
+ handler(200) { |resp| resp.body.upcase }
7
+ end
8
+
9
+ action :headered, 'GET /headered' do
10
+ before_request { |req| req.headers['Added-Header'] = self.value }
11
+ end
12
+ end
13
+
14
+ def value
15
+ scope.value
16
+ end
17
+ end
18
+
19
+ RSpec.describe 'Resource Actions' do
20
+ let(:connection) { Faraday.new { |b| b.adapter :test, stubs } }
21
+ let(:scoped) { double('scope', value: 'bunk') }
22
+ let(:stubs) do
23
+ Faraday::Adapter::Test::Stubs.new do |stub|
24
+ stub.get('/dummy') { |env| [200, {}, 'dummies'] }
25
+ stub.get('/headered') { |env| [200, {}, env[:request_headers]['Added-Header']] }
26
+ end
27
+ end
28
+
29
+ it 'Retrieving /dummy returns the body as uppercased' do
30
+ resource = DummyResourceActions.new(connection: connection, scope: scoped)
31
+ response = resource.dummy
32
+ expect(response).to eq('DUMMIES')
33
+ end
34
+
35
+ it 'adds the header before the request happens' do
36
+ resource = DummyResourceActions.new(connection: connection, scope: scoped)
37
+ response = resource.headered
38
+
39
+ expect(response).to eq(scoped.value)
40
+ end
41
+ end
@@ -0,0 +1,167 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe ResourceSet::ActionInvoker do
4
+ let(:connection) { Faraday.new { |b| b.adapter :test, stubs } }
5
+ let(:stubs) do
6
+ Faraday::Adapter::Test::Stubs.new do |stub|
7
+ stub.get('/users') { |env| [200, {}, 'all users'] }
8
+ stub.get('/users/bad_page') { |env| [404, {}, 'not found'] }
9
+ stub.get('/users/12') { |env| [200, {}, 'user 12'] }
10
+ stub.post('/users') { |env| [200, {}, env[:body]] }
11
+ stub.get('/paged') { |env| [200, {}, env[:url].to_s] }
12
+ stub.get('/before_hooks') { |env| [200, {}, env[:request_headers]['Owner-Id']] }
13
+ stub.get('/block_based') { |env| [200, {}, 'block based path'] }
14
+ end
15
+ end
16
+ let(:action) { ResourceSet::Action.new(:find) }
17
+ let(:resource) { double('resource instance', connection: connection) }
18
+
19
+ describe '.call' do
20
+ it 'performs a request to the specfied URL' do
21
+ action.verb :get
22
+ action.path '/users'
23
+
24
+ result = ResourceSet::ActionInvoker.call(action, resource)
25
+
26
+ expect(result).to eq('all users')
27
+ end
28
+
29
+ it 'performs a request to the correct url when using a block for path' do
30
+ action.path { '/block_based' }
31
+ result = ResourceSet::ActionInvoker.call(action, resource)
32
+
33
+ expect(result).to eq('block based path')
34
+ end
35
+
36
+ it 'substitues params on call' do
37
+ action.verb :get
38
+ action.path '/users/:id'
39
+
40
+ result = ResourceSet::ActionInvoker.call(action, resource, id: 12)
41
+ expect(result).to eq('user 12')
42
+ end
43
+
44
+ context 'when an action has a handler' do
45
+ it 'returns the handler block' do
46
+ action.verb :get
47
+ action.path '/users'
48
+ action.handler(200) { |response| 'changed' }
49
+
50
+ result = ResourceSet::ActionInvoker.call(action, resource)
51
+
52
+ expect(result).to eq('changed')
53
+ end
54
+
55
+ it 'passes the initial object' do
56
+ action.verb :get
57
+ action.path '/users'
58
+ action.handler(200) { |response, hash| hash }
59
+
60
+ target_hash = Hash.new
61
+ result = ResourceSet::ActionInvoker.call(action, resource, target_hash)
62
+
63
+ expect(result).to be target_hash
64
+ end
65
+
66
+ it 'uses the correct handler on status codes' do
67
+ action.verb :get
68
+ action.path '/users/bad_page'
69
+ action.handler(404) { |response| '404ed' }
70
+
71
+ result = ResourceSet::ActionInvoker.call(action, resource)
72
+ expect(result).to eq('404ed')
73
+ end
74
+
75
+ it 'uses a default handler if provided' do
76
+ action.verb :get
77
+ action.path '/users'
78
+ action.handler { |response| 'Something unexpected happened.' }
79
+
80
+ result = ResourceSet::ActionInvoker.call(action, resource)
81
+ expect(result).to eq('Something unexpected happened.')
82
+ end
83
+ end
84
+
85
+ context 'for requests with bodies' do
86
+ it 'uses the body handler when present' do
87
+ action.verb :post
88
+ action.path '/users'
89
+ action.body { |object| 'i am a banana' }
90
+
91
+ result = ResourceSet::ActionInvoker.call(action, resource, 'echo me')
92
+ expect(result).to eq('i am a banana')
93
+ end
94
+
95
+ it 'uses the body handler with multiple arity when present' do
96
+ action.verb :post
97
+ action.path '/users'
98
+ action.body { |first, second| first + second }
99
+
100
+ result = ResourceSet::ActionInvoker.call(action, resource, 'echo me', ' another')
101
+ expect(result).to eq('echo me another')
102
+ end
103
+ end
104
+
105
+ context 'for requests with query params' do
106
+ it 'appends the query parameters to the endpoint' do
107
+ action.query_keys :per_page, :page
108
+ action.path '/paged'
109
+
110
+ result = ResourceSet::ActionInvoker.call(action, resource, page: 3, per_page: 300)
111
+ addressed = Addressable::URI.parse(result)
112
+
113
+ expect(addressed.query_values).to include('per_page' => '300', 'page' => '3')
114
+ end
115
+ end
116
+
117
+ context 'for actions that have before request hooks' do
118
+ it 'calls the before request with the request object' do
119
+ action.path '/before_hooks'
120
+ action.verb :get
121
+ action.before_request { |req| req.headers['Owner-Id'] = 'bojangles' }
122
+
123
+ result = ResourceSet::ActionInvoker.call(action, resource)
124
+ expect(result).to eq('bojangles')
125
+ end
126
+
127
+ it 'calls the before request with the request object and arguments' do
128
+ action.path '/before_hooks'
129
+ action.verb :get
130
+ action.before_request { |one, two, req| req.headers['Owner-Id'] = "#{one} #{two}" }
131
+
132
+ result = ResourceSet::ActionInvoker.call(action, resource, 'one', 'two')
133
+ expect(result).to eq('one two')
134
+ end
135
+
136
+ context 'for passing a symbol' do
137
+ it 'calls the method on the context of the action' do
138
+ def resource.kickit(request)
139
+ request.headers['Owner-Id'] = 'btabes'
140
+ end
141
+
142
+ action.path '/before_hooks'
143
+ action.verb :get
144
+ action.before_request(:kickit)
145
+
146
+ invoker = ResourceSet::ActionInvoker.new(action, resource)
147
+
148
+ expect(invoker.handle_response).to eq('btabes')
149
+ end
150
+
151
+ it 'calls the action with arguments passed' do
152
+ def resource.kickit(one, two, request)
153
+ request.headers['Owner-Id'] = "#{one} #{two}"
154
+ end
155
+
156
+ action.path '/before_hooks'
157
+ action.verb :get
158
+ action.before_request(:kickit)
159
+
160
+ invoker = ResourceSet::ActionInvoker.new(action, resource, 'bobby', 'tables')
161
+
162
+ expect(invoker.handle_response).to eq('bobby tables')
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,87 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe ResourceSet::Action do
4
+ subject(:action) { ResourceSet::Action.new(:bunk) }
5
+
6
+ describe '#initialize' do
7
+ it 'initializes with a name' do
8
+ instance = ResourceSet::Action.new(:all)
9
+ expect(instance.name).to eq(:all)
10
+ end
11
+ end
12
+
13
+ describe '#verb' do
14
+ it 'sets the verb' do
15
+ action = described_class.new(:find)
16
+ action.verb :get
17
+ expect(action.verb).to be(:get)
18
+ end
19
+ end
20
+
21
+ describe '#path' do
22
+ it 'sets the path' do
23
+ action = described_class.new(:find)
24
+ action.path '/something/sammy'
25
+ expect(action.path).to eq('/something/sammy')
26
+ end
27
+
28
+ it 'sets the path to a block when passed' do
29
+ action = described_class.new(:find)
30
+ proc = Proc.new { '/users/:id/comments' }
31
+ action.path(&proc)
32
+
33
+ expect(action.path).to be(proc)
34
+ end
35
+ end
36
+
37
+ describe '#handler' do
38
+ it 'adds a handler to the handlers' do
39
+ expect { action.handler(202) { } }.to change(action.handlers, :size).from(0).to(1)
40
+ end
41
+
42
+ it 'adds the correct status code when using a symbol' do
43
+ action.handler(:ok) {}
44
+ expect(action.handlers).to have_key(200)
45
+ end
46
+ end
47
+
48
+ describe '#handlers' do
49
+ it 'returns a handler collection object' do
50
+ collection = action.handlers
51
+ expect(collection).to be_kind_of(Hash)
52
+ end
53
+ end
54
+
55
+ describe '#body' do
56
+ it 'stores a proc for handling requests with bodies' do
57
+ handler = Proc.new { |object| 'whut whut' }
58
+ action.body(&handler)
59
+
60
+ expect(action.body).to be(handler)
61
+ end
62
+ end
63
+
64
+ describe '#query_keys' do
65
+ it 'allows setting known query parameters that we should append to the URL' do
66
+ action.query_keys :per_page, :page
67
+ expect(action.query_keys).to include(:per_page, :page)
68
+ end
69
+ end
70
+
71
+ describe '#before_request' do
72
+ context 'with a block' do
73
+ it 'sets a block to happen before the request happens' do
74
+ proc = Proc.new {}
75
+ action.before_request(&proc)
76
+ expect(action.hooks[:before]).to include(proc)
77
+ end
78
+ end
79
+
80
+ context 'with a symbol' do
81
+ it 'adds the symbol to the before hooks' do
82
+ action.before_request(:foo)
83
+ expect(action.hooks[:before]).to include(:foo)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe ResourceSet::EndpointResolver do
4
+ describe '#initialize' do
5
+ it 'initializes with pieces of a URL' do
6
+ options = { path: '/users', namespace: '/v2', query_param_keys: ['per_page', 'page'] }
7
+ instance = ResourceSet::EndpointResolver.new(options)
8
+
9
+ expect(instance.path).to eq(options[:path])
10
+ expect(instance.query_param_keys).to eq(options[:query_param_keys])
11
+ end
12
+ end
13
+
14
+ describe '#resolve' do
15
+ let(:path) { '/users' }
16
+ let(:query_param_keys) { [] }
17
+ let(:options) { { path: path, query_param_keys: query_param_keys } }
18
+
19
+ subject(:resolver) { ResourceSet::EndpointResolver.new(options) }
20
+
21
+ context 'simple resolve' do
22
+ it 'creates a populated URL from passed values' do
23
+ endpoint = resolver.resolve()
24
+ expect(endpoint).to eq('/users')
25
+ end
26
+ end
27
+
28
+ context 'substituted paths' do
29
+ let(:path) { '/users/:id' }
30
+
31
+ it 'creates a populated URL from passed values' do
32
+ endpoint = resolver.resolve(id: 1066)
33
+ expect(endpoint).to eq('/users/1066')
34
+ end
35
+ end
36
+
37
+ context 'with query parameters' do
38
+ let(:query_param_keys) { [:per_page, :page] }
39
+
40
+ it 'generates a URL with query parameters set correctly' do
41
+ endpoint = resolver.resolve(per_page: 2, page: 3)
42
+
43
+ uri = Addressable::URI.parse(endpoint)
44
+ expect(uri.query_values).to eq("per_page" => '2', "page" => '3')
45
+ end
46
+ end
47
+
48
+ context 'with query parameters already appended' do
49
+ let(:path) { '/:something/users?foo=bar' }
50
+ let(:query_param_keys) { [:per_page, :page] }
51
+
52
+ it 'appends the query params to the url that already has some' do
53
+ endpoint = resolver.resolve(something: 'testing', per_page: 2, page: 3)
54
+
55
+ uri = Addressable::URI.parse(endpoint)
56
+ expect(uri.query_values).to eq("foo" => 'bar', "per_page" => '2', "page" => '3')
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+
3
+ describe ResourceSet::InheritableAttribute do
4
+ subject do
5
+ Class.new(Object) do
6
+ extend ResourceSet::InheritableAttribute
7
+
8
+ inheritable_attr :_resources
9
+ end
10
+ end
11
+
12
+ it 'provides a reader with an empty inherited attribute' do
13
+ expect(subject._resources).to be_nil
14
+ end
15
+
16
+ it 'provides a reader with empty inherited attributes in a derived class' do
17
+ expect(Class.new(subject)._resources).to be_nil
18
+
19
+ # subject._resouces = true
20
+ # Class.new(subject)._resources # TODO: crashes.
21
+ end
22
+
23
+ it 'provides an attribute copy in subclasses' do
24
+ subject._resources = []
25
+ expect(subject._resources.object_id).not_to eq Class.new(subject)._resources.object_id
26
+ end
27
+
28
+ it 'provides a writer' do
29
+ subject._resources = [:resource]
30
+ expect(subject._resources).to eq [:resource]
31
+ end
32
+
33
+ it 'inherits attributes' do
34
+ subject._resources = [:resource]
35
+
36
+ subclass_a = Class.new(subject)
37
+ subclass_a._resources << :another_resource
38
+
39
+ subclass_b = Class.new(subject)
40
+ subclass_b._resources << :different_resource
41
+
42
+ expect(subject._resources).to eq [:resource]
43
+ expect(subclass_a._resources).to eq [:resource, :another_resource]
44
+ expect(subclass_b._resources).to eq [:resource, :different_resource]
45
+ end
46
+
47
+ it 'does not inherit attributes if we set explicitely' do
48
+ subject._resources = [:resource]
49
+ subclass = Class.new(subject)
50
+
51
+ subclass._resources = [:another_resource]
52
+ expect(subclass._resources).to eq [:another_resource]
53
+ end
54
+ end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe ResourceSet::MethodFactory do
4
+ let(:collection) { ResourceSet::ResourceCollection.new }
5
+ let(:klass) { Class.new { attr_accessor :connection } }
6
+
7
+ describe '.construct' do
8
+ before do
9
+ collection.action :find
10
+ collection.action :all
11
+ end
12
+
13
+ it 'adds the methods from the resource collection to the passed object' do
14
+ ResourceSet::MethodFactory.construct(klass, collection)
15
+ instance = klass.new
16
+ expect(instance).to respond_to(:find, :all)
17
+ end
18
+
19
+ it 'bails when the method is already defined' do
20
+ collection.action :all
21
+
22
+ expect {
23
+ ResourceSet::MethodFactory.construct(klass, collection)
24
+ }.to raise_exception(ArgumentError).with_message("Action 'all' is already defined on `#{klass}`")
25
+ end
26
+
27
+ it 'adds the correct interface for the action' do
28
+ ResourceSet::MethodFactory.construct(klass, collection)
29
+ method_sig = klass.instance_method(:all).parameters
30
+
31
+ expect(method_sig).to eq([[:rest, :args]])
32
+ end
33
+
34
+ end
35
+
36
+ describe 'Calling the method' do
37
+ it 'calls the invoker passed with the arguments and action' do
38
+ action = ResourceSet::Action.new(:bunk)
39
+ collection << action
40
+ invoker = double('invoker', call: true)
41
+
42
+ ResourceSet::MethodFactory.construct(klass, collection, invoker)
43
+
44
+ instance = klass.new
45
+ instance.connection = double('connection')
46
+ instance.bunk('something', 'something else')
47
+ expect(invoker).to have_received(:call).with(action, instance, 'something', 'something else')
48
+ end
49
+ end
50
+ end