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,12 @@
1
+ require 'hashie'
2
+
3
+ module Sagamore
4
+ class Client
5
+ ##
6
+ # An indifferent Hash to represent parsed response bodies.
7
+ #
8
+ # @see http://rdoc.info/github/intridea/hashie/Hashie/Mash See the Hashie::Mash docs for usage details
9
+ class Body < ::Hashie::Mash
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,108 @@
1
+ module Sagamore
2
+ class Client
3
+ ##
4
+ # Mixin provides {Resource} with special methods for convenient interaction
5
+ # with collection resources.
6
+ module Collection
7
+ include ::Enumerable
8
+
9
+ ##
10
+ # Iterates over all results from the collection and yields each one to
11
+ # the block, fetching pages as needed.
12
+ #
13
+ # @raise [RequestFailed]
14
+ def each(&block)
15
+ call_client(:each, &block)
16
+ end
17
+
18
+ ##
19
+ # Iterates over each page of results and yields the page to the block,
20
+ # fetching as needed.
21
+ #
22
+ # @raise [RequestFailed]
23
+ def each_page(&block)
24
+ call_client(:each_page, &block)
25
+ end
26
+
27
+ ##
28
+ # Performs a request and returns the number of resources in the collection.
29
+ #
30
+ # @raise [RequestFailed]
31
+ #
32
+ # @return [Integer] The subordinate resource count
33
+ def count
34
+ call_client(:count)
35
+ end
36
+
37
+ ##
38
+ # Returns true if count is greater than zero, else false.
39
+ #
40
+ # @see #count
41
+ #
42
+ # @raise [RequestFailed]
43
+ #
44
+ # @return [Boolean]
45
+ def empty?
46
+ count <= 0
47
+ end
48
+
49
+ ##
50
+ # Returns a new resource with the given filters added to the query string.
51
+ #
52
+ # @see https://github.com/sagamore/sagamore-retail/blob/master/api/doc/filtering.md Sagamore collection API filtering docs
53
+ #
54
+ # @param [String, Hash] new_filters Hash or JSON string of new filters
55
+ #
56
+ # @return [Resource]
57
+ def filter(new_filters)
58
+ new_filters = JSON.parse(new_filters) if new_filters.is_a?(String)
59
+ if filters = query['_filter']
60
+ filters = JSON.parse(filters)
61
+ filters = [filters] unless filters.is_a?(Array)
62
+ filters.push(new_filters)
63
+ else
64
+ filters = new_filters
65
+ end
66
+ query('_filter' => filters.to_json)
67
+ end
68
+
69
+ ##
70
+ # Returns a new resource with the given sorts added to the query string.
71
+ #
72
+ # @example
73
+ # resource.sort('id,desc', 'name', 'custom@category,desc', :description)
74
+ #
75
+ # @param [#to_s] sorts One or more sort strings
76
+ #
77
+ # @return [Resource]
78
+ def sort(*sorts)
79
+ query('sort' => sorts)
80
+ end
81
+
82
+ ##
83
+ # Performs a request to get the first result of the first page of the
84
+ # collection and returns it.
85
+ #
86
+ # @raise [RequestFailed]
87
+ #
88
+ # @return [Body] The first entry in the response :results array
89
+ def first
90
+ response = query(:per_page => 1, :page => 1).get!
91
+ response[:results].first
92
+ end
93
+
94
+ ##
95
+ # Performs repeated GET requests to the resource and yields results to
96
+ # the given block as long as the response includes more results.
97
+ #
98
+ # @raise [RequestFailed]
99
+ def while_results(&block)
100
+ loop do
101
+ results = get![:results]
102
+ break if results.nil? || results.empty?
103
+ results.each(&block)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,17 @@
1
+ module Sagamore
2
+ class Client
3
+ ##
4
+ # API request failure
5
+ class RequestFailed < RuntimeError
6
+ attr_accessor :response
7
+ end
8
+
9
+ ##
10
+ # Authorization failure
11
+ class AuthFailed < RequestFailed; end
12
+
13
+ ##
14
+ # Error parsing the response body
15
+ class BodyError < RequestFailed; end
16
+ end
17
+ end
@@ -0,0 +1,205 @@
1
+ require 'sagamore/client/collection'
2
+
3
+ module Sagamore
4
+ class Client
5
+ ##
6
+ # An representation of an API resource identified by a URI. Allows
7
+ # triggering API calls via HTTP methods. Allows constructing new resources
8
+ # in a chained style by calling the methods that manipulate the URI and
9
+ # return a new resource.
10
+ #
11
+ # @example Chaining
12
+ # new_resource = resource.
13
+ # filter(:field => 'value').
14
+ # sort(:id).
15
+ # embed(:related_resource)
16
+ #
17
+ # Resources are usually constructed via the Client#[] method.
18
+ class Resource
19
+ ##
20
+ # The resource's URI.
21
+ #
22
+ # @return [Addressable::URI]
23
+ attr_reader :uri
24
+
25
+ ##
26
+ # The underlying Sagamore Client.
27
+ #
28
+ # @return [Client]
29
+ attr_reader :client
30
+
31
+ ##
32
+ # @param [Sagamore::Client] client
33
+ # @param [Addressable::URI, #to_s] uri
34
+ def initialize(client, uri)
35
+ @client = client
36
+ @uri = URI.join('/', uri.to_s)
37
+ end
38
+
39
+ ##
40
+ # Performs a HEAD request against the resource's URI and returns the Response.
41
+ #
42
+ # @return [Response]
43
+ def head(headers=false); call_client(:head, headers); end
44
+
45
+ ##
46
+ # Performs a HEAD request against the resource's URI. Returns the Response
47
+ # on success and raises a RequestFailed on failure..
48
+ #
49
+ # @raise [RequestFailed] On error response
50
+ #
51
+ # @return [Response]
52
+ def head!(headers=false); call_client(:head!, headers); end
53
+
54
+ ##
55
+ # Performs a GET request against the resource's URI and returns the Response.
56
+ #
57
+ # @return [Response]
58
+ def get(headers=false); call_client(:get, headers); end
59
+
60
+ ##
61
+ # Performs a GET request against the resource's URI. Returns the Response
62
+ # on success and raises a RequestFailed on failure.
63
+ #
64
+ # @raise [RequestFailed] On error response
65
+ #
66
+ # @return [Response]
67
+ def get!(headers=false); call_client(:get!, headers); end
68
+
69
+ ##
70
+ # Performs a DELETE request against the resource's URI and returns the Response.
71
+ #
72
+ # @return [Response]
73
+ def delete(headers=false); call_client(:delete, headers); end
74
+
75
+ ##
76
+ # Performs a DELETE request against the resource's URI. Returns the Response
77
+ # on success and raises a RequestFailed on failure.
78
+ #
79
+ # @raise [RequestFailed] On error response
80
+ #
81
+ # @return [Response]
82
+ def delete!(headers=false); call_client(:delete!, headers); end
83
+
84
+ ##
85
+ # Performs a PUT request against the resource's URI and returns the Response.
86
+ #
87
+ # @return [Response]
88
+ def put(body, headers=false); call_client(:put, body, headers); end
89
+
90
+ ##
91
+ # Performs a PUT request against the resource's URI. Returns the Response
92
+ # on success and raises a RequestFailed on failure.
93
+ #
94
+ # @raise [RequestFailed] On error response
95
+ #
96
+ # @return [Response]
97
+ def put!(body, headers=false); call_client(:put!, body, headers); end
98
+
99
+ ##
100
+ # Performs a POST request against the resource's URI and returns the Response.
101
+ #
102
+ # @return [Response]
103
+ def post(body, headers=false); call_client(:post, body, headers); end
104
+
105
+ ##
106
+ # Performs a POST request against the resource's URI. Returns the Response
107
+ # on success and raises a RequestFailed on failure.
108
+ #
109
+ # @raise [RequestFailed] On error response
110
+ #
111
+ # @return [Response]
112
+ def post!(body, headers=false); call_client(:post!, body, headers); end
113
+
114
+ ##
115
+ # Returns a new subordinate resource with the given sub-path.
116
+ #
117
+ # @return [Resource]
118
+ def [](uri)
119
+ clone(self.uri.subpath(uri))
120
+ end
121
+
122
+ ##
123
+ # If called with +params+ as a +Hash+:
124
+ #
125
+ # Returns a new resource where the given query hash
126
+ # is merged with the existing query string parameters.
127
+ #
128
+ # If called with no arguments:
129
+ #
130
+ # Returns the resource's current query string parameters and values
131
+ # as a hash.
132
+ #
133
+ # @return [Resource, Hash]
134
+
135
+ ##
136
+ # @overload query(params)
137
+ # Returns a new resource where the given +params+ hash of parameter
138
+ # names and values is merged with the existing query string parameters.
139
+ #
140
+ # @param [Hash] params New query string parameters
141
+ # @return [Resource]
142
+ #
143
+ # @overload query()
144
+ # Returns the resource's current query string parameters and values
145
+ # as a hash.
146
+ #
147
+ # @return [Hash]
148
+ def query(params=nil)
149
+ if params
150
+ uri = self.uri.dup
151
+ uri.merge_query_values!(params)
152
+ clone(uri)
153
+ else
154
+ self.uri.query_values || {}
155
+ end
156
+ end
157
+
158
+ alias params query
159
+
160
+ ##
161
+ # Returns a cloned copy of the resource with the same URI.
162
+ #
163
+ # @return [Resource]
164
+ def clone(uri=nil)
165
+ self.class.new(client, uri ? uri : self.uri)
166
+ end
167
+
168
+ ##
169
+ # Returns a new resource with the given embeds added to the query string
170
+ # (via _include params).
171
+ #
172
+ # @return [Resource]
173
+ def embed(*embeds)
174
+ embeds = (query['_include'] || []) + embeds
175
+ query('_include' => embeds)
176
+ end
177
+
178
+ ##
179
+ # Returns true if a HEAD request to the resource returns a successful response,
180
+ # false if it returns 404, otherwise raises an exception.
181
+ #
182
+ # @raise [RequestFailed] If response is not success or 404
183
+ #
184
+ # @return [Boolean]
185
+ def exists?
186
+ response = head
187
+ return true if response.success?
188
+ return false if response.status == 404
189
+ error = RequestFailed.new "Request during call to 'exists?' resulted in non-404 error."
190
+ error.response = response
191
+ raise error
192
+ end
193
+
194
+ include Collection
195
+
196
+ private
197
+
198
+ ##
199
+ # Calls a client method, passing the URI as the first argument.
200
+ def call_client(method, *args, &block)
201
+ client.__send__(method, *args.unshift(uri), &block)
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,85 @@
1
+ module Sagamore
2
+ class Client
3
+ ##
4
+ # An API response including body, headers, and status information.
5
+ class Response
6
+ ##
7
+ # @param [Response] response
8
+ # @param [Client] client
9
+ def initialize(response, client)
10
+ @response = response
11
+ @client = client
12
+ end
13
+
14
+ ##
15
+ # Returns the corresponding key from "body".
16
+ def [](key)
17
+ body[key]
18
+ end
19
+
20
+ ##
21
+ # Returns the raw response body as a String.
22
+ #
23
+ # @return [String] The raw response body
24
+ def raw_body
25
+ @response.body
26
+ end
27
+
28
+ ##
29
+ # Returns the parsed response body as a Body object.
30
+ #
31
+ # @raise [BodyError] If the body is not parseable
32
+ #
33
+ # @return [Body] The parsed response body
34
+ def body
35
+ @data ||= parse_body
36
+ end
37
+
38
+ ##
39
+ # Returns true if the request was successful, else false.
40
+ #
41
+ # @return [Boolean]
42
+ def success?
43
+ status < 400
44
+ end
45
+
46
+ ##
47
+ # Delegates missing methods to the underlying Patron::Response.
48
+ #
49
+ # @see http://patron.rubyforge.org/Patron/Response.html Patron::Response docs
50
+ def method_missing(method, *args, &block)
51
+ @response.respond_to?(method) ? @response.__send__(method, *args, &block) : super
52
+ end
53
+
54
+ ##
55
+ # If the response included a 'Location' header, returns a new Resource with
56
+ # a URI set to its value, else nil.
57
+ #
58
+ # @return [Resource]
59
+ def resource
60
+ if location = headers['Location']
61
+ @client[headers['Location']]
62
+ else
63
+ nil
64
+ end
65
+ end
66
+
67
+ protected
68
+
69
+ def parse_body
70
+ if @response.body.empty?
71
+ raise BodyError,
72
+ "Response body is empty. (Hint: If you just created a new resource, try: response.resource.get)"
73
+ end
74
+
75
+ begin
76
+ data = JSON.parse(@response.body)
77
+ Body.new data
78
+ rescue JSON::ParserError => e
79
+ raise BodyError, "Can't parse response body. (Hint: Try the raw_body method.)"
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+
@@ -0,0 +1,51 @@
1
+ module Sagamore
2
+ class Client
3
+ ##
4
+ # Extensions to the Addressable::URI class.
5
+ module URIExt
6
+ ##
7
+ # Returns a new URI with the given subpath appended to it. Ensures a single
8
+ # forward slash between the URI's path and the given subpath.
9
+ def subpath(subpath)
10
+ uri = dup
11
+ uri.path = "#{path}/" unless path.end_with?('/')
12
+ uri.join subpath.to_s.gsub(/^\//, '')
13
+ end
14
+
15
+ ##
16
+ # Merges the given hash of query string parameters and values with the URI's
17
+ # existing query string parameters (if any).
18
+ def merge_query_values!(values)
19
+ self.sagamore_query_values = (self.query_values || {}).merge(normalize_query_hash(values))
20
+ end
21
+
22
+ private
23
+
24
+ def sagamore_query_values=(values)
25
+ retval = self.query_values = normalize_query_hash(values)
26
+ # Hack to strip digits from Addressable::URI's subscript notation
27
+ self.query = self.query.gsub(/\[\d+\]=/, '[]=')
28
+ retval
29
+ end
30
+
31
+ def normalize_query_hash(hash)
32
+ hash.inject({}) do |copy, (k, v)|
33
+ copy[k.to_s] = case v
34
+ when Hash then normalize_query_hash(v)
35
+ when true, false then v.to_s
36
+ else v end
37
+ copy
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ ##
45
+ # We include Sagamore::Client::URIExt into Addressable::URI because its design
46
+ # doesn't support subclassing.
47
+ #
48
+ # @see http://addressable.rubyforge.org/api/Addressable/URI.html Addressable::URI docs
49
+ class Addressable::URI
50
+ include Sagamore::Client::URIExt
51
+ end