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.
- checksums.yaml +15 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/.yardopts +1 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +66 -0
- data/LICENSE +21 -0
- data/README.md +252 -0
- data/Rakefile +16 -0
- data/lib/sagamore-client.rb +1 -0
- data/lib/sagamore/client.rb +263 -0
- data/lib/sagamore/client/body.rb +12 -0
- data/lib/sagamore/client/collection.rb +108 -0
- data/lib/sagamore/client/errors.rb +17 -0
- data/lib/sagamore/client/resource.rb +205 -0
- data/lib/sagamore/client/response.rb +85 -0
- data/lib/sagamore/client/uri_ext.rb +51 -0
- data/sagamore-client.gemspec +20 -0
- data/spec/sagamore/client/body_spec.rb +32 -0
- data/spec/sagamore/client/resource_spec.rb +250 -0
- data/spec/sagamore/client/response_spec.rb +100 -0
- data/spec/sagamore/client/uri_ext_spec.rb +51 -0
- data/spec/sagamore/client_spec.rb +214 -0
- data/spec/shared_client_context.rb +5 -0
- data/spec/spec_helper.rb +12 -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-1.2.0.gem +0 -0
- data/vendor/cache/json-1.7.4.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 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
|