resource_set 1.0.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 +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