resource_set 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|