springboard-retail 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +66 -0
- data/LICENSE +21 -0
- data/README.md +253 -0
- data/Rakefile +16 -0
- data/lib/springboard-retail.rb +1 -0
- data/lib/springboard/client.rb +263 -0
- data/lib/springboard/client/body.rb +12 -0
- data/lib/springboard/client/collection.rb +108 -0
- data/lib/springboard/client/errors.rb +17 -0
- data/lib/springboard/client/resource.rb +205 -0
- data/lib/springboard/client/response.rb +85 -0
- data/lib/springboard/client/uri_ext.rb +51 -0
- data/spec/shared_client_context.rb +5 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/springboard/client/body_spec.rb +32 -0
- data/spec/springboard/client/resource_spec.rb +250 -0
- data/spec/springboard/client/response_spec.rb +100 -0
- data/spec/springboard/client/uri_ext_spec.rb +51 -0
- data/spec/springboard/client_spec.rb +214 -0
- data/springboard-retail.gemspec +20 -0
- data/vendor/cache/addressable-2.2.8.gem +0 -0
- data/vendor/cache/coderay-1.0.7.gem +0 -0
- data/vendor/cache/coveralls-0.6.9.gem +0 -0
- data/vendor/cache/crack-0.3.1.gem +0 -0
- data/vendor/cache/diff-lcs-1.1.3.gem +0 -0
- data/vendor/cache/hashie-2.0.5.gem +0 -0
- data/vendor/cache/json-1.8.1.gem +0 -0
- data/vendor/cache/method_source-0.8.gem +0 -0
- data/vendor/cache/mime-types-1.25.gem +0 -0
- data/vendor/cache/multi_json-1.8.0.gem +0 -0
- data/vendor/cache/patron-0.4.18.gem +0 -0
- data/vendor/cache/pry-0.9.10.gem +0 -0
- data/vendor/cache/rake-0.9.2.2.gem +0 -0
- data/vendor/cache/rest-client-1.6.7.gem +0 -0
- data/vendor/cache/rspec-2.11.0.gem +0 -0
- data/vendor/cache/rspec-core-2.11.1.gem +0 -0
- data/vendor/cache/rspec-expectations-2.11.2.gem +0 -0
- data/vendor/cache/rspec-mocks-2.11.1.gem +0 -0
- data/vendor/cache/simplecov-0.7.1.gem +0 -0
- data/vendor/cache/simplecov-html-0.7.1.gem +0 -0
- data/vendor/cache/slop-3.3.2.gem +0 -0
- data/vendor/cache/term-ansicolor-1.2.2.gem +0 -0
- data/vendor/cache/thor-0.18.1.gem +0 -0
- data/vendor/cache/tins-0.9.0.gem +0 -0
- data/vendor/cache/webmock-1.8.8.gem +0 -0
- 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
|