leadlight 0.0.2

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 (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