leadlight 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/Gemfile +9 -0
  2. data/Gemfile.lock +79 -0
  3. data/Guardfile +19 -0
  4. data/Rakefile +133 -0
  5. data/leadlight.gemspec +125 -0
  6. data/lib/leadlight.rb +92 -0
  7. data/lib/leadlight/blank.rb +15 -0
  8. data/lib/leadlight/codec.rb +63 -0
  9. data/lib/leadlight/enumerable_representation.rb +24 -0
  10. data/lib/leadlight/errors.rb +14 -0
  11. data/lib/leadlight/hyperlinkable.rb +126 -0
  12. data/lib/leadlight/link.rb +35 -0
  13. data/lib/leadlight/link_template.rb +47 -0
  14. data/lib/leadlight/representation.rb +30 -0
  15. data/lib/leadlight/request.rb +91 -0
  16. data/lib/leadlight/service.rb +73 -0
  17. data/lib/leadlight/service_middleware.rb +50 -0
  18. data/lib/leadlight/tint.rb +26 -0
  19. data/lib/leadlight/tint_helper.rb +67 -0
  20. data/lib/leadlight/type.rb +71 -0
  21. data/spec/cassettes/Leadlight/authorized_GitHub_example/_user/has_the_expected_content.yml +75 -0
  22. data/spec/cassettes/Leadlight/authorized_GitHub_example/_user/indicates_the_expected_oath_scopes.yml +75 -0
  23. data/spec/cassettes/Leadlight/authorized_GitHub_example/adding_and_removing_team_members.yml +384 -0
  24. data/spec/cassettes/Leadlight/authorized_GitHub_example/adding_and_removing_team_members/.yml +309 -0
  25. data/spec/cassettes/Leadlight/authorized_GitHub_example/test_team/.yml +159 -0
  26. data/spec/cassettes/Leadlight/basic_GitHub_example/_root/.yml +32 -0
  27. data/spec/cassettes/Leadlight/basic_GitHub_example/_root/__location__/.yml +32 -0
  28. data/spec/cassettes/Leadlight/basic_GitHub_example/_root/should_be_a_204_no_content.yml +32 -0
  29. data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/.yml +32 -0
  30. data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/__location__/.yml +32 -0
  31. data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/should_be_a_204_no_content.yml +32 -0
  32. data/spec/cassettes/Leadlight/tinted_GitHub_example/_user/has_the_expected_content.yml +65 -0
  33. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/.yml +100 -0
  34. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_able_to_follow_next_link.yml +135 -0
  35. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_enumerable.yml +275 -0
  36. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_enumerable_over_page_boundaries.yml +170 -0
  37. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_have_next_and_last_links.yml +100 -0
  38. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_link/exists.yml +32 -0
  39. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_link/links_to_the_expected_URL.yml +32 -0
  40. data/spec/leadlight/codec_spec.rb +93 -0
  41. data/spec/leadlight/hyperlinkable_spec.rb +136 -0
  42. data/spec/leadlight/link_spec.rb +53 -0
  43. data/spec/leadlight/link_template_spec.rb +45 -0
  44. data/spec/leadlight/representation_spec.rb +62 -0
  45. data/spec/leadlight/request_spec.rb +236 -0
  46. data/spec/leadlight/service_middleware_spec.rb +81 -0
  47. data/spec/leadlight/service_spec.rb +127 -0
  48. data/spec/leadlight/tint_helper_spec.rb +132 -0
  49. data/spec/leadlight/type_spec.rb +137 -0
  50. data/spec/leadlight_spec.rb +237 -0
  51. data/spec/spec_helper_lite.rb +6 -0
  52. data/spec/support/credentials.rb +16 -0
  53. data/spec/support/vcr.rb +18 -0
  54. metadata +229 -0
@@ -0,0 +1,236 @@
1
+ require 'spec_helper_lite'
2
+ require 'leadlight/request'
3
+ require 'timeout'
4
+
5
+ module Leadlight
6
+ describe Request do
7
+ include Timeout
8
+
9
+
10
+ # The Faraday connection API works like this:
11
+ #
12
+ # connection.run_request(:get, ...).on_complete do |env|
13
+ # ...
14
+ # end
15
+ #
16
+ # TODO stick a facade over Faraday instead of stubbing stuff I
17
+ # don't own
18
+ class FakeFaradayResponse
19
+ fattr(:completion_handlers) { Queue.new }
20
+ attr_reader :env
21
+
22
+ def initialize(env)
23
+ @env = env
24
+ end
25
+
26
+ def on_complete(&block)
27
+ completion_handlers << block
28
+ end
29
+
30
+ def run_completion_handlers(n=1)
31
+ n.times do
32
+ completion_handlers.pop.call(@env)
33
+ end
34
+ end
35
+ end
36
+
37
+ subject { Request.new(connection, url, http_method, params, body) }
38
+ let(:connection) { stub(:connection, :run_request => faraday_response) }
39
+ let(:url) { stub(:url) }
40
+ let(:http_method){ :get }
41
+ let(:body) { stub(:body) }
42
+ let(:params) { {} }
43
+ let(:faraday_request) {stub(:faraday_request)}
44
+ let(:on_complete_handlers) { [] }
45
+ let(:faraday_response) { FakeFaradayResponse.new(faraday_env) }
46
+ let(:faraday_env) { {:leadlight_representation => representation} }
47
+ let(:representation) { stub(:representation) }
48
+
49
+ def run_completion_handlers
50
+ faraday_response.run_completion_handlers
51
+ end
52
+
53
+ def do_it_and_complete(&block)
54
+ t = Thread.new do
55
+ do_it(&block)
56
+ end
57
+ run_completion_handlers
58
+ t.join.value
59
+ end
60
+
61
+ context "for GET" do
62
+ let(:http_method) { :get }
63
+
64
+ its(:http_method) { should eq(:get) }
65
+ end
66
+
67
+
68
+ context "for POST" do
69
+ let(:http_method) { :post }
70
+
71
+ its(:http_method) { should eq(:post) }
72
+ end
73
+
74
+ describe "#submit" do
75
+ it "starts a request runnning" do
76
+ connection.should_receive(:run_request).
77
+ with(http_method, url, body, {}).
78
+ and_return(faraday_response)
79
+ subject.submit
80
+ end
81
+
82
+ it "triggers the on_prepare_request hook in the block passed to #run_request" do
83
+ yielded = :nothing
84
+ faraday_request = stub
85
+ connection.stub(:run_request).
86
+ and_yield(faraday_request).
87
+ and_return(faraday_response)
88
+ subject.on_prepare_request do |request|
89
+ yielded = request
90
+ end
91
+ subject.submit
92
+ yielded.should equal(faraday_request)
93
+ end
94
+
95
+ end
96
+
97
+ shared_examples_for "synchronous methods" do
98
+ it "returns once the request has completed" do
99
+ timeout(1) do
100
+ trace = Queue.new
101
+ thread = Thread.new do
102
+ do_it
103
+ trace << "wait finished"
104
+ end
105
+ trace << "completing request"
106
+ faraday_response.run_completion_handlers
107
+ thread.join
108
+ trace << "request completed"
109
+ trace.pop.should eq("completing request")
110
+ trace.pop.should eq("wait finished")
111
+ trace.pop.should eq("request completed")
112
+ end
113
+ end
114
+ end
115
+
116
+ describe "#wait" do
117
+ context "before a submit" do
118
+ it "doesn't return until after the submit" do
119
+ timeout(1) do
120
+ trace = Queue.new
121
+ thread = Thread.new do
122
+ subject.wait
123
+ trace << "wait finished"
124
+ end
125
+ trace << "submit"
126
+ subject.submit
127
+ trace << "completing request"
128
+ faraday_response.run_completion_handlers
129
+ thread.join
130
+ trace << "request completed"
131
+ trace.pop.should eq("submit")
132
+ trace.pop.should eq("completing request")
133
+ trace.pop.should eq("wait finished")
134
+ trace.pop.should eq("request completed")
135
+ end
136
+ end
137
+ end
138
+
139
+ context "after a submit" do
140
+ before do
141
+ subject.submit
142
+ end
143
+
144
+ def do_it
145
+ subject.wait
146
+ end
147
+
148
+ it_should_behave_like "synchronous methods"
149
+
150
+ it "returns self" do
151
+ do_it_and_complete.should equal(subject)
152
+ end
153
+ end
154
+
155
+ end
156
+
157
+ describe "#submit_and_wait" do
158
+ def do_it(&block)
159
+ subject.submit_and_wait(&block)
160
+ end
161
+
162
+ it_should_behave_like "synchronous methods"
163
+
164
+ it "returns self" do
165
+ do_it_and_complete.should equal(subject)
166
+ end
167
+
168
+ it "yields the response representation" do
169
+ yielded = :nothing
170
+ do_it_and_complete do |rep|
171
+ yielded = rep
172
+ end
173
+ yielded.should equal(representation)
174
+ end
175
+ end
176
+
177
+ describe "#on_complete" do
178
+ def submit_and_complete
179
+ t = Thread.new do
180
+ subject.submit
181
+ subject.wait
182
+ end
183
+ run_completion_handlers
184
+ t.join
185
+ end
186
+
187
+ it "queues hooks to be run on completion" do
188
+ run_hooks = []
189
+ subject.on_complete do |response|
190
+ run_hooks << "hook 1"
191
+ end
192
+ subject.on_complete do |response|
193
+ run_hooks << "hook 2"
194
+ end
195
+ submit_and_complete
196
+ run_hooks.should eq(["hook 1", "hook 2"])
197
+ end
198
+
199
+ it "calls hooks with the faraday response" do
200
+ Faraday::Response.should_receive(:new).with(faraday_env).
201
+ and_return(faraday_response)
202
+ yielded = :nothing
203
+ subject.on_complete do |response|
204
+ yielded = response
205
+ end
206
+ submit_and_complete
207
+ yielded.should equal(faraday_response)
208
+ end
209
+ end
210
+
211
+
212
+ describe "#raise_on_error" do
213
+ def submit_and_complete
214
+ t = Thread.new do
215
+ subject.submit
216
+ subject.wait
217
+ end
218
+ run_completion_handlers
219
+ t.join
220
+ subject
221
+ end
222
+
223
+ it "raises an error when the response is a client error" do
224
+ faraday_env[:status] = 404
225
+ expect { submit_and_complete.raise_on_error }.to raise_error(ResourceNotFound)
226
+ end
227
+
228
+ it "raises after completion when called before completion" do
229
+ faraday_env[:status] = 500
230
+ subject.raise_on_error
231
+ expect { submit_and_complete }.to raise_error(ServerError)
232
+ end
233
+
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,81 @@
1
+ require 'spec_helper_lite'
2
+ require 'leadlight/service_middleware'
3
+ require 'faraday'
4
+
5
+ module Leadlight
6
+ describe ServiceMiddleware do
7
+ let(:test_stack) {
8
+ Faraday.new(url: 'http://example.com') do |b|
9
+ b.use described_class, service: service
10
+ b.adapter :test, faraday_stubs
11
+ end
12
+ }
13
+
14
+ subject { described_class.new(app, service: service) }
15
+ let(:service) { stub(url: 'http://example.com', tints: []) }
16
+ let(:faraday_stubs) { Faraday::Adapter::Test::Stubs.new }
17
+ let(:app) { ->(env){stub(on_complete: nil)} }
18
+ let(:response) { [200, {}, ''] }
19
+ let(:result_env) {
20
+ test_stack.get('/').env
21
+ }
22
+ let(:representation) { result_env[:leadlight_representation] }
23
+
24
+ before do
25
+ faraday_stubs.get('/') {response}
26
+ end
27
+
28
+ it 'adds :leadlight_service to env before app' do
29
+ test_stack.get('/').env[:leadlight_service].should equal(service)
30
+ end
31
+
32
+ it 'extends the representation with Representation' do
33
+ representation.should be_a(Representation)
34
+ end
35
+
36
+ it 'makes the representation hyperlinkable' do
37
+ representation.should be_a(Hyperlinkable)
38
+ end
39
+
40
+ it 'requests JSON, then YAML, then XML, then HTML' do
41
+ result_env[:request_headers]['Accept'].
42
+ should eq('application/json, text/x-yaml, application/xml, application/xhtml+xml, text/html, text/plain')
43
+ end
44
+
45
+ context 'with a no-content response' do
46
+ let(:response) { [204, {}, ''] }
47
+ let(:result_env) {
48
+ test_stack.get('/').env
49
+ }
50
+
51
+ it 'sets :leadlight_representation to a Blank' do
52
+ result_env[:leadlight_representation].should be_a(Blank)
53
+ end
54
+ end
55
+
56
+ context 'with a blank JSON response' do
57
+ let(:response) {
58
+ [200, {'Content-Type' => 'application/json', 'Content-Length' => '0'}, '']
59
+ }
60
+ let(:result_env) {
61
+ test_stack.get('/').env
62
+ }
63
+
64
+ it 'sets :leadlight_representation to a Blank' do
65
+ result_env[:leadlight_representation].should be_a(Blank)
66
+ end
67
+ end
68
+
69
+ context 'with a non-blank JSON response' do
70
+ let(:response) {
71
+ [200, {'Content-Type' => 'application/json', 'Content-Length' => '7'}, '[1,2,3]']
72
+ }
73
+ let(:result_env) {
74
+ test_stack.get('/').env
75
+ }
76
+ it 'sets :leadlight_representation to the result of parsing the JSON' do
77
+ result_env[:leadlight_representation].should eq([1,2,3])
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,127 @@
1
+ require 'spec_helper_lite'
2
+ require 'leadlight/service'
3
+
4
+ module Leadlight
5
+ describe Service do
6
+ subject { klass.new(service_options) }
7
+ let(:klass) { Class.new do include Service end }
8
+ let(:connection) { stub(:connection, get: response) }
9
+ let(:representation) { stub(:representation) }
10
+ let(:response) { stub(:response, env: env) }
11
+ let(:env) { {leadlight_representation: representation} }
12
+ let(:service_options) { {codec: codec} }
13
+ let(:codec) { stub(:codec) }
14
+ let(:request) { stub(:request).as_null_object }
15
+
16
+ before do
17
+ subject.stub(connection: connection, url: nil)
18
+ end
19
+
20
+ shared_examples_for "an HTTP client" do |http_method|
21
+ describe "##{http_method}" do
22
+ before do
23
+ Request.stub(:new).and_return(request)
24
+ end
25
+
26
+ it 'returns a new request object' do
27
+ Request.should_receive(:new).and_return(request)
28
+ subject.public_send(http_method, '/').should equal(request)
29
+ end
30
+
31
+ it 'passes the connection to the request' do
32
+ Request.should_receive(:new).
33
+ with(connection, anything, anything, anything, anything).
34
+ and_return(request)
35
+ subject.public_send(http_method, '/somepath')
36
+ end
37
+
38
+ it 'passes the path to the request' do
39
+ Request.should_receive(:new).
40
+ with(anything, '/somepath', anything, anything, anything).
41
+ and_return(request)
42
+ subject.public_send(http_method, '/somepath')
43
+ end
44
+
45
+ it 'passes the method to the request' do
46
+ Request.should_receive(:new).
47
+ with(anything, anything, http_method, anything, anything).
48
+ and_return(request)
49
+ subject.public_send(http_method, '/somepath')
50
+ end
51
+
52
+ it 'passes the params to the request' do
53
+ params = stub
54
+ Request.should_receive(:new).
55
+ with(anything, anything, anything, params, anything).
56
+ and_return(request)
57
+ subject.public_send(http_method, '/somepath', params)
58
+ end
59
+
60
+ it 'passes the body to the request' do
61
+ body = stub
62
+ Request.should_receive(:new).
63
+ with(anything, anything, anything, anything, body).
64
+ and_return(request)
65
+ subject.public_send(http_method, '/somepath', {}, body)
66
+ end
67
+
68
+ it 'adds a prepare_request callback' do
69
+ faraday_request = stub(:faraday_request)
70
+ request.stub(:on_prepare_request).and_yield(faraday_request)
71
+ subject.should_receive(:prepare_request).with(faraday_request)
72
+ subject.public_send(http_method, '/')
73
+ end
74
+
75
+ context 'given a block' do
76
+ define_method(:do_it) do
77
+ subject.public_send(http_method, '/') do |yielded|
78
+ return yielded
79
+ end
80
+ end
81
+
82
+ it 'submits and waits for completion' do
83
+ request.should_receive(:submit_and_wait).and_yield(representation)
84
+ do_it
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ it_behaves_like "an HTTP client", :head
91
+ it_behaves_like "an HTTP client", :get
92
+ it_behaves_like "an HTTP client", :post
93
+ it_behaves_like "an HTTP client", :put
94
+ it_behaves_like "an HTTP client", :delete
95
+ it_behaves_like "an HTTP client", :patch
96
+ it_behaves_like "an HTTP client", :options
97
+
98
+ describe '#service_options' do
99
+ it 'returns option values passed to the initializer' do
100
+ it = klass.new(foo: 42, bar: 'baz')
101
+ it.service_options.should eq(foo: 42, bar: 'baz')
102
+ end
103
+ end
104
+
105
+ describe '#encode' do
106
+ it 'delegates to the codec' do
107
+ entity = stub
108
+ codec.should_receive(:encode).
109
+ with('CONTENT_TYPE', representation, {foo: 'bar'}).
110
+ and_return(entity)
111
+ subject.encode('CONTENT_TYPE', representation, {foo: 'bar'}).
112
+ should equal(entity)
113
+ end
114
+ end
115
+
116
+ describe '#decode' do
117
+ it 'delegates to the codec' do
118
+ entity = stub
119
+ codec.should_receive(:decode).
120
+ with('CONTENT_TYPE', entity, {foo: 'bar'}).
121
+ and_return(representation)
122
+ subject.decode('CONTENT_TYPE', entity, {foo: 'bar'}).
123
+ should equal(representation)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,132 @@
1
+ require 'spec_helper_lite'
2
+ require 'leadlight/tint_helper'
3
+
4
+ module Leadlight
5
+ describe TintHelper do
6
+ subject{ TintHelper.new(object, tint) }
7
+ let(:object) {
8
+ stub(__location__: stub(path: '/the/path'),
9
+ __response__: response)
10
+ }
11
+ let(:response) {
12
+ stub(:response,
13
+ env: env)
14
+ }
15
+ let(:env) { {response_headers: headers} }
16
+ let(:headers) {
17
+ { 'Content-Type' => 'text/html; charset=UTF-8'}
18
+ }
19
+ let(:tint) { Module.new }
20
+
21
+ it 'forwards unknown calls to the wrapped object' do
22
+ object.should_receive(:foo).with('bar')
23
+ subject.foo('bar')
24
+ end
25
+
26
+ describe '#match_path' do
27
+ it 'allows execution to proceed on match' do
28
+ object.should_receive(:baz)
29
+ subject.exec_tint do
30
+ match_path('/the/path')
31
+ baz
32
+ end
33
+ end
34
+
35
+ it 'can match on a Regexp' do
36
+ object.should_receive(:baz)
37
+ subject.exec_tint do
38
+ match_path(/path/)
39
+ baz
40
+ end
41
+ end
42
+
43
+ it 'does not allow execution to proceed on no match' do
44
+ object.should_not_receive(:baz)
45
+ subject.exec_tint do
46
+ match_path('/the/wrong/path')
47
+ baz
48
+ end
49
+ end
50
+ end
51
+
52
+ describe '#match_content_type' do
53
+ it 'allows execution to proceed on match' do
54
+ object.should_receive(:baz)
55
+ subject.exec_tint do
56
+ match_content_type('text/html')
57
+ baz
58
+ end
59
+ end
60
+
61
+ it 'can match on a regex' do
62
+ object.should_receive(:baz)
63
+ subject.exec_tint do
64
+ match_content_type(/text/)
65
+ baz
66
+ end
67
+ end
68
+
69
+ it 'does not allow execution to proceed on no match' do
70
+ object.should_not_receive(:baz)
71
+ subject.exec_tint do
72
+ match_content_type('text/plain')
73
+ baz
74
+ end
75
+ end
76
+ end
77
+
78
+ describe '#match' do
79
+ it 'allows execution to proceed on true return' do
80
+ object.should_receive(:baz)
81
+ subject.exec_tint do
82
+ match{ (2 + 2) == 4 }
83
+ baz
84
+ end
85
+ end
86
+
87
+ it 'does not allow execution to proceed on false return' do
88
+ object.should_not_receive(:baz)
89
+ subject.exec_tint do
90
+ match{ (2 + 2) == 5 }
91
+ baz
92
+ end
93
+ end
94
+ end
95
+
96
+ describe '#exec_tint' do
97
+ it 'returns self' do
98
+ subject.exec_tint{}.should equal(subject)
99
+ end
100
+ end
101
+
102
+ describe '#add_header' do
103
+ it 'adds a header' do
104
+ subject.add_header('MyHeader', 'Hello world')
105
+ headers['MyHeader'].should eq('Hello world')
106
+ end
107
+ end
108
+
109
+ describe '#extend' do
110
+ context 'given a module' do
111
+ let(:mod) { Module.new }
112
+
113
+ it 'extends the object with the given module' do
114
+ subject.extend(mod)
115
+ object.should be_a(mod)
116
+ end
117
+ end
118
+
119
+ context 'given a block' do
120
+ it 'extends the tint module with the given definitions' do
121
+ subject.extend do
122
+ def magic_word
123
+ 'xyzzy'
124
+ end
125
+ end
126
+ object.magic_word.should eq('xyzzy')
127
+ end
128
+ end
129
+ end
130
+
131
+ end
132
+ end