springboard-retail 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -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 +253 -0
  10. data/Rakefile +16 -0
  11. data/lib/springboard-retail.rb +1 -0
  12. data/lib/springboard/client.rb +263 -0
  13. data/lib/springboard/client/body.rb +12 -0
  14. data/lib/springboard/client/collection.rb +108 -0
  15. data/lib/springboard/client/errors.rb +17 -0
  16. data/lib/springboard/client/resource.rb +205 -0
  17. data/lib/springboard/client/response.rb +85 -0
  18. data/lib/springboard/client/uri_ext.rb +51 -0
  19. data/spec/shared_client_context.rb +5 -0
  20. data/spec/spec_helper.rb +12 -0
  21. data/spec/springboard/client/body_spec.rb +32 -0
  22. data/spec/springboard/client/resource_spec.rb +250 -0
  23. data/spec/springboard/client/response_spec.rb +100 -0
  24. data/spec/springboard/client/uri_ext_spec.rb +51 -0
  25. data/spec/springboard/client_spec.rb +214 -0
  26. data/springboard-retail.gemspec +20 -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-2.0.5.gem +0 -0
  33. data/vendor/cache/json-1.8.1.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 Springboard
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 Springboard
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/springboard/springboard-retail/blob/master/api/doc/filtering.md Springboard 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 Springboard
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 'springboard/client/collection'
2
+
3
+ module Springboard
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 Springboard Client.
27
+ #
28
+ # @return [Client]
29
+ attr_reader :client
30
+
31
+ ##
32
+ # @param [Springboard::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 Springboard
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 Springboard
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.springboard_query_values = (self.query_values || {}).merge(normalize_query_hash(values))
20
+ end
21
+
22
+ private
23
+
24
+ def springboard_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 Springboard::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 Springboard::Client::URIExt
51
+ end