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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +2 -0
- data/.travis.yml +12 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +165 -0
- data/Rakefile +5 -0
- data/examples/digitalocean_droplets.rb +60 -0
- data/examples/httpbin_client.rb +15 -0
- data/lib/resource_set/action.rb +66 -0
- data/lib/resource_set/action_invoker.rb +58 -0
- data/lib/resource_set/endpoint_resolver.rb +46 -0
- data/lib/resource_set/inheritable_attribute.rb +20 -0
- data/lib/resource_set/method_factory.rb +20 -0
- data/lib/resource_set/resource.rb +40 -0
- data/lib/resource_set/resource_collection.rb +55 -0
- data/lib/resource_set/status_code_mapper.rb +59 -0
- data/lib/resource_set/testing/action_handler_matchers.rb +42 -0
- data/lib/resource_set/testing/have_action_matchers.rb +85 -0
- data/lib/resource_set/testing.rb +20 -0
- data/lib/resource_set/version.rb +3 -0
- data/lib/resource_set.rb +17 -0
- data/resource_set.gemspec +30 -0
- data/spec/integration/resource_actions_spec.rb +41 -0
- data/spec/lib/resource_set/action_invoker_spec.rb +167 -0
- data/spec/lib/resource_set/action_spec.rb +87 -0
- data/spec/lib/resource_set/endpoint_resolver_spec.rb +60 -0
- data/spec/lib/resource_set/inheritable_attribute_spec.rb +54 -0
- data/spec/lib/resource_set/method_factory_spec.rb +50 -0
- data/spec/lib/resource_set/resource_collection_spec.rb +67 -0
- data/spec/lib/resource_set/resource_spec.rb +66 -0
- data/spec/lib/resource_set/status_code_mapper_spec.rb +9 -0
- data/spec/lib/resource_set/testing/action_handler_matchers_spec.rb +68 -0
- data/spec/lib/resource_set/testing/have_action_matchers_spec.rb +157 -0
- data/spec/spec_helper.rb +8 -0
- 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
|