sagamore-client 3.0.1

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 (52) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +6 -0
  5. data/.yardopts +1 -0
  6. data/Gemfile +14 -0
  7. data/Gemfile.lock +66 -0
  8. data/LICENSE +21 -0
  9. data/README.md +252 -0
  10. data/Rakefile +16 -0
  11. data/lib/sagamore-client.rb +1 -0
  12. data/lib/sagamore/client.rb +263 -0
  13. data/lib/sagamore/client/body.rb +12 -0
  14. data/lib/sagamore/client/collection.rb +108 -0
  15. data/lib/sagamore/client/errors.rb +17 -0
  16. data/lib/sagamore/client/resource.rb +205 -0
  17. data/lib/sagamore/client/response.rb +85 -0
  18. data/lib/sagamore/client/uri_ext.rb +51 -0
  19. data/sagamore-client.gemspec +20 -0
  20. data/spec/sagamore/client/body_spec.rb +32 -0
  21. data/spec/sagamore/client/resource_spec.rb +250 -0
  22. data/spec/sagamore/client/response_spec.rb +100 -0
  23. data/spec/sagamore/client/uri_ext_spec.rb +51 -0
  24. data/spec/sagamore/client_spec.rb +214 -0
  25. data/spec/shared_client_context.rb +5 -0
  26. data/spec/spec_helper.rb +12 -0
  27. data/vendor/cache/addressable-2.2.8.gem +0 -0
  28. data/vendor/cache/coderay-1.0.7.gem +0 -0
  29. data/vendor/cache/coveralls-0.6.9.gem +0 -0
  30. data/vendor/cache/crack-0.3.1.gem +0 -0
  31. data/vendor/cache/diff-lcs-1.1.3.gem +0 -0
  32. data/vendor/cache/hashie-1.2.0.gem +0 -0
  33. data/vendor/cache/json-1.7.4.gem +0 -0
  34. data/vendor/cache/method_source-0.8.gem +0 -0
  35. data/vendor/cache/mime-types-1.25.gem +0 -0
  36. data/vendor/cache/multi_json-1.8.0.gem +0 -0
  37. data/vendor/cache/patron-0.4.18.gem +0 -0
  38. data/vendor/cache/pry-0.9.10.gem +0 -0
  39. data/vendor/cache/rake-0.9.2.2.gem +0 -0
  40. data/vendor/cache/rest-client-1.6.7.gem +0 -0
  41. data/vendor/cache/rspec-2.11.0.gem +0 -0
  42. data/vendor/cache/rspec-core-2.11.1.gem +0 -0
  43. data/vendor/cache/rspec-expectations-2.11.2.gem +0 -0
  44. data/vendor/cache/rspec-mocks-2.11.1.gem +0 -0
  45. data/vendor/cache/simplecov-0.7.1.gem +0 -0
  46. data/vendor/cache/simplecov-html-0.7.1.gem +0 -0
  47. data/vendor/cache/slop-3.3.2.gem +0 -0
  48. data/vendor/cache/term-ansicolor-1.2.2.gem +0 -0
  49. data/vendor/cache/thor-0.18.1.gem +0 -0
  50. data/vendor/cache/tins-0.9.0.gem +0 -0
  51. data/vendor/cache/webmock-1.8.8.gem +0 -0
  52. metadata +148 -0
@@ -0,0 +1,20 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "sagamore-client"
3
+ s.version = "3.0.1"
4
+ s.platform = Gem::Platform::RUBY
5
+ s.authors = ["Sagamore"]
6
+ s.summary = "Sagamore client library"
7
+
8
+ s.required_rubygems_version = ">= 1.3.6"
9
+
10
+ s.add_runtime_dependency 'patron', '~> 0.4.18'
11
+ s.add_runtime_dependency 'addressable', '~> 2.2.8'
12
+ s.add_runtime_dependency 'json', '~> 1.7.4'
13
+ s.add_runtime_dependency 'hashie'
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+
17
+ s.require_path = 'lib'
18
+ end
19
+
20
+
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sagamore::Client::Body do
4
+ let(:hash) { {"key1" => "val1", "key2" => {"subkey1" => "subval1"}} }
5
+ let(:body) { Sagamore::Client::Body.new(hash)}
6
+
7
+ describe "[]" do
8
+ it "should support string keys" do
9
+ body["key1"].should == "val1"
10
+ end
11
+
12
+ it "should support symbol keys" do
13
+ body[:key1].should == "val1"
14
+ end
15
+ end
16
+
17
+ describe "nested hashes" do
18
+ it "should support nested indifferent access" do
19
+ body[:key2][:subkey1].should == "subval1"
20
+ body['key2']['subkey1'].should == "subval1"
21
+ body[:key2]['subkey1'].should == "subval1"
22
+ body['key2'][:subkey1].should == "subval1"
23
+ end
24
+ end
25
+
26
+ describe "to_hash" do
27
+ it "should return the original hash" do
28
+ body.to_hash.should === hash
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1,250 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sagamore::Client::Resource do
4
+ include_context "client"
5
+
6
+ let(:resource_path) { '/some/path' }
7
+ let(:resource) { Sagamore::Client::Resource.new(client, resource_path) }
8
+
9
+ def parse_uri(uri)
10
+ Addressable::URI.parse(uri)
11
+ end
12
+
13
+ describe "[]" do
14
+ it "should return a new resource" do
15
+ resource["subpath"].should be_a Sagamore::Client::Resource
16
+ resource["subpath"].object_id.should_not == resource.object_id
17
+ end
18
+
19
+ it "should return a resource with the given subpath appended to its URI" do
20
+ resource["subpath"].uri.to_s.should == "/some/path/subpath"
21
+ end
22
+
23
+ it "should return a resource with the same client instance" do
24
+ resource["subpath"].client.should === resource.client
25
+ end
26
+
27
+ it "should accept a symbol as a path" do
28
+ resource[:subpath].uri.to_s.should == "/some/path/subpath"
29
+ end
30
+
31
+ it "should accept a symbol as a path" do
32
+ resource[:subpath].uri.to_s.should == "/some/path/subpath"
33
+ end
34
+
35
+ it "should not URI encode the given subpath" do
36
+ resource["subpath with spaces"].uri.to_s.should == "/some/path/subpath with spaces"
37
+ end
38
+ end
39
+
40
+ %w{query params}.each do |method|
41
+ describe method do
42
+ describe "when called with a hash" do
43
+ it "should set the query string parameters" do
44
+ resource.__send__(method, :a => 1, :b => 2).uri.to_s.should == "/some/path?a=1&b=2"
45
+ end
46
+
47
+ it "should URL encode the given keys and values" do
48
+ resource.__send__(method, "i have spaces" => "so do i: duh").uri.to_s.
49
+ should == "/some/path?i%20have%20spaces=so%20do%20i%3A%20duh"
50
+ end
51
+
52
+ it "should add bracket notation for array parameters" do
53
+ resource.__send__(method, :somearray => [1, 2, 3]).uri.to_s.should == "/some/path?somearray[]=1&somearray[]=2&somearray[]=3"
54
+ end
55
+ end
56
+
57
+ describe "when called without arguments" do
58
+ it "should return the current query string parameters as a hash" do
59
+ resource.__send__(method).should == {}
60
+ new_resource = resource.__send__(method, :a => 1, :b => 2)
61
+ new_resource.__send__(method).should == {"a"=>"1", "b"=>"2"}
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ describe "filter" do
68
+ describe "when given a hash" do
69
+ it "should add a _filter query string param" do
70
+ resource.filter(:a => 1, :b => 2).uri.should ==
71
+ '/some/path?_filter={"a":1,"b":2}'.to_uri
72
+ end
73
+ end
74
+
75
+ describe "when called multiple times" do
76
+ it "should append args to _filter param as JSON array" do
77
+ resource.filter(:a => 1).filter(:b => 2).filter(:c => 3).uri.should ==
78
+ '/some/path?_filter=[{"a":1},{"b":2},{"c":3}]'.to_uri
79
+ end
80
+ end
81
+
82
+ describe "when given a string" do
83
+ it "should add a _filter query string param" do
84
+ resource.filter('{"a":1,"b":2}').uri.should ==
85
+ '/some/path?_filter={"a":1,"b":2}'.to_uri
86
+ end
87
+ end
88
+ end
89
+
90
+ describe "sort" do
91
+ it "should set the sort parameter based on the given values" do
92
+ resource.sort('f1', 'f2,desc').uri.query.should == 'sort[]=f1&sort[]=f2%2Cdesc'
93
+ end
94
+
95
+ it "should replace any existing sort parameter" do
96
+ resource.sort('f1', 'f2,desc')
97
+ resource.sort('f3,asc', 'f4').uri.query.should == 'sort[]=f3%2Casc&sort[]=f4'
98
+ end
99
+ end
100
+
101
+ %w{count each each_page}.each do |method|
102
+ describe method do
103
+ it "should call the client's #{method} method with the resource's URI" do
104
+ client.should_receive(method).with(resource.uri)
105
+ resource.__send__(method)
106
+ end
107
+ end
108
+ end
109
+
110
+ %w{get head delete}.each do |method|
111
+ describe method do
112
+ it "should call the client's #{method} method with the resource's URI and a header hash" do
113
+ client.should_receive(method).with(resource.uri, false)
114
+ resource.__send__(method)
115
+ end
116
+ end
117
+ end
118
+
119
+ %w{put post}.each do |method|
120
+ describe method do
121
+ it "should call the client's #{method} method with the resource's URI, the given body, and a headers hash" do
122
+ client.should_receive(method).with(resource.uri, "body", false)
123
+ resource.__send__(method, "body")
124
+ end
125
+ end
126
+ end
127
+
128
+ describe "first" do
129
+ let(:response_data) {
130
+ {
131
+ :status => 200,
132
+ :body => {:results => [{:id => "Me first!"}, {:id => "Me second!"}]}.to_json
133
+ }
134
+ }
135
+
136
+ it "should set the per_page query string param to 1" do
137
+ request_stub = stub_request(:get, "#{base_url}/some/path?page=1&per_page=1").to_return(response_data)
138
+ resource.first
139
+ request_stub.should have_been_requested
140
+ end
141
+
142
+ it "should return the first element of the :results array" do
143
+ request_stub = stub_request(:get, "#{base_url}/some/path?page=1&per_page=1").to_return(response_data)
144
+ resource.first.should == {"id" => "Me first!"}
145
+ end
146
+ end
147
+
148
+ describe "embed" do
149
+ it "should support a single embed" do
150
+ resource.embed(:thing1).uri.to_s.should ==
151
+ '/some/path?_include[]=thing1'
152
+ end
153
+
154
+ it "should support multiple embeds" do
155
+ resource.embed(:thing1, :thing2, :thing3).uri.to_s.should ==
156
+ '/some/path?_include[]=thing1&_include[]=thing2&_include[]=thing3'
157
+ end
158
+
159
+ it "should merge multiple embed calls" do
160
+ resource.embed(:thing1, :thing2).embed(:thing3, :thing4).uri.to_s.should ==
161
+ '/some/path?_include[]=thing1&_include[]=thing2&_include[]=thing3&_include[]=thing4'
162
+ end
163
+
164
+ it "should merge multiple embed calls" do
165
+ resource.embed(:thing1, :thing2).embed(:thing3, :thing4).uri.to_s.should ==
166
+ '/some/path?_include[]=thing1&_include[]=thing2&_include[]=thing3&_include[]=thing4'
167
+ end
168
+
169
+ it "should merge a call to embed with a manually added _include query param" do
170
+ resource.query('_include[]' => :thing1).embed(:thing2, :thing3).uri.to_s.should ==
171
+ '/some/path?_include[]=thing1&_include[]=thing2&_include[]=thing3'
172
+ end
173
+ end
174
+
175
+ describe "while_results" do
176
+ it "should yield each result to the block as long as the response includes results" do
177
+ results = ["r1", "r2", "r3"]
178
+
179
+ request_stub = stub_request(:get, "#{base_url}/some/path").to_return do |req|
180
+ {:body => {:results => results}.to_json}
181
+ end
182
+
183
+ yielded_results = []
184
+
185
+ # timeout in case of endless loop
186
+ Timeout::timeout(1) do
187
+ resource.while_results do |result|
188
+ yielded_results.push results.shift
189
+ end
190
+ end
191
+
192
+ yielded_results.should == ["r1", "r2", "r3"]
193
+ end
194
+
195
+ it "should raise an exception if it receives an error response" do
196
+ request_stub = stub_request(:get, "#{base_url}/some/path").to_return do |req|
197
+ {:status => 400}
198
+ end
199
+
200
+ # timeout in case of endless loop
201
+ Timeout::timeout(1) do
202
+ expect do
203
+ resource.while_results do |result|
204
+ # nothing
205
+ end
206
+ end.to raise_error(Sagamore::Client::RequestFailed)
207
+ end
208
+ end
209
+
210
+ describe "exists?" do
211
+ let(:response) { mock(Sagamore::Client::Response) }
212
+
213
+ it "should return true if the response indicates success" do
214
+ response.stub!(:success?).and_return(true)
215
+ client.should_receive(:head).with(resource.uri, false).and_return(response)
216
+ resource.exists?.should === true
217
+ end
218
+
219
+ it "should return false if the response status is 404" do
220
+ response.stub!(:status).and_return(404)
221
+ response.stub!(:success?).and_return(false)
222
+ client.should_receive(:head).with(resource.uri, false).and_return(response)
223
+ resource.exists?.should === false
224
+ end
225
+
226
+ it "should raise a RequestFailed exception if the request fails but the status is not 404" do
227
+ response.stub!(:status).and_return(400)
228
+ response.stub!(:success?).and_return(false)
229
+ client.should_receive(:head).with(resource.uri, false).and_return(response)
230
+ expect { resource.exists? }.to raise_error { |e|
231
+ e.should be_a Sagamore::Client::RequestFailed
232
+ e.response.should === response
233
+ e.message.should == "Request during call to 'exists?' resulted in non-404 error."
234
+ }
235
+ end
236
+ end
237
+
238
+ describe "empty?" do
239
+ it "should return true if the resource has a count of zero" do
240
+ resource.stub!(:count).and_return 0
241
+ resource.empty?.should === true
242
+ end
243
+
244
+ it "should return false if the resource has a count greater than zero" do
245
+ resource.stub!(:count).and_return 10
246
+ resource.empty?.should === false
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,100 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sagamore::Client::Response do
4
+ include_context "client"
5
+
6
+ let(:raw_body) { '{"key":"value"}' }
7
+ let(:raw_headers) { 'X-Custom-Header: Hi' }
8
+ let(:status_code) { 200 }
9
+ let(:path) { '/path' }
10
+ let(:patron_response) { Patron::Response.new(path, status_code, 0, raw_headers, raw_body) }
11
+ let(:response) { Sagamore::Client::Response.new(patron_response, client) }
12
+
13
+ describe "body" do
14
+ describe "if raw body is valid JSON" do
15
+ it "should return a Sagamore::Client::Body" do
16
+ response.body.should be_a Sagamore::Client::Body
17
+ end
18
+
19
+ it "should wrap the parsed response body" do
20
+ response.body.to_hash.should == {"key" => "value"}
21
+ end
22
+ end
23
+
24
+ describe "if raw body is not valid JSON" do
25
+ let(:raw_body) { 'I am not JSON!' }
26
+ it "should raise an informative error" do
27
+ expect { response.body }.to raise_error \
28
+ Sagamore::Client::BodyError,
29
+ "Can't parse response body. (Hint: Try the raw_body method.)"
30
+ end
31
+ end
32
+
33
+ describe "if raw body is empty" do
34
+ let(:raw_body) { '' }
35
+ it "should raise an informative error" do
36
+ expect { response.body }.to raise_error \
37
+ Sagamore::Client::BodyError,
38
+ "Response body is empty. (Hint: If you just created a new resource, try: response.resource.get)"
39
+ end
40
+ end
41
+ end
42
+
43
+ describe "raw_body" do
44
+ it "should return the raw body JSON" do
45
+ response.raw_body.should == raw_body
46
+ end
47
+ end
48
+
49
+ describe "headers" do
50
+ it "should return the response headers as a hash" do
51
+ response.headers.should == {'X-Custom-Header' => 'Hi'}
52
+ end
53
+ end
54
+
55
+ describe "resource" do
56
+ describe "when Location header is returned" do
57
+ let(:raw_headers) { 'Location: /new/path' }
58
+
59
+ it "should be a Sagamore::Client::Resource" do
60
+ response.resource.should be_a Sagamore::Client::Resource
61
+ end
62
+
63
+ it "should have the Location header value as its URL" do
64
+ response.resource.uri.to_s.should == '/new/path'
65
+ end
66
+ end
67
+
68
+ describe "when Location header is not returned" do
69
+ let(:raw_headers) { '' }
70
+
71
+ it "should be nil" do
72
+ response.resource.should be_nil
73
+ end
74
+ end
75
+ end
76
+
77
+ describe "[]" do
78
+ it "should forward [] to body" do
79
+ response.body.should_receive(:[]).with("key").and_return("value")
80
+ response["key"].should == "value"
81
+ end
82
+ end
83
+
84
+ describe "success?" do
85
+ %w{100 101 102 200 201 202 203 204 205 206 207 208 226 300 301 302 303 304 305 306 307 308}.each do |code|
86
+ it "should return true if the response status code is #{code}" do
87
+ response.stub!(:status).and_return(code.to_i)
88
+ response.success?.should === true
89
+ end
90
+ end
91
+
92
+ %w{400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 420 422 423 424 425 426 428 429 431
93
+ 444 449 450 499 500 501 502 503 504 505 506 507 508 509 510 511 598 599}.each do |code|
94
+ it "should return false if the response status code is #{code}" do
95
+ response.stub!(:status).and_return(code.to_i)
96
+ response.success?.should === false
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe Addressable::URI do
4
+ let(:uri) { Addressable::URI.parse('/relative/path') }
5
+
6
+ describe "subpath" do
7
+ it "should return a new URI with the path relative to the receiver" do
8
+ uri.subpath('other').should == Addressable::URI.parse('/relative/path/other')
9
+ uri.subpath('/other').should == Addressable::URI.parse('/relative/path/other')
10
+ uri.subpath(Addressable::URI.parse('/other')) == Addressable::URI.parse('/relative/path/other')
11
+ end
12
+ end
13
+
14
+ describe "merge_query_values!" do
15
+ it "should call sagamore_query_values=" do
16
+ uri.query_values = {'a' => '1'}
17
+ uri.should_receive(:sagamore_query_values=).with({'a' => '1', 'b' => '2'})
18
+ uri.merge_query_values! 'b' => '2'
19
+ end
20
+
21
+ it "should merge the given values with the existing query_values" do
22
+ uri.query_values = {'a' => '1', 'b' => '2'}
23
+ uri.merge_query_values! 'b' => '20', 'c' => '30'
24
+ uri.query_values.should == {'a' => '1', 'b' => '20', 'c' => '30'}
25
+ end
26
+
27
+ it "should set the given values if there are no existing query_values" do
28
+ uri.query_values.should be_nil
29
+ uri.merge_query_values! 'b' => '20', 'c' => '30'
30
+ uri.query_values.should == {'b' => '20', 'c' => '30'}
31
+ end
32
+ end
33
+
34
+ describe "sagamore_query_values=" do
35
+ it "should preserve empty bracket notation for array params" do
36
+ uri.query = 'sort[]=f1&sort[]=f2'
37
+ uri.__send__(:sagamore_query_values=, uri.query_values)
38
+ uri.to_s.should == '/relative/path?sort[]=f1&sort[]=f2'
39
+ end
40
+
41
+ it "should stringify boolean param values" do
42
+ uri.__send__(:sagamore_query_values=, {:p1 => true, :p2 => false})
43
+ uri.to_s.should == '/relative/path?p1=true&p2=false'
44
+ end
45
+
46
+ it "should support hash param values" do
47
+ uri.__send__(:sagamore_query_values=, {:a => {:b => {:c => 123}}})
48
+ uri.to_s.should == '/relative/path?a[b][c]=123'
49
+ end
50
+ end
51
+ end