heartland-retail 5.0.0

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 (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +16 -0
  5. data/.yardopts +1 -0
  6. data/Gemfile +15 -0
  7. data/Gemfile.lock +84 -0
  8. data/LICENSE +21 -0
  9. data/README.md +272 -0
  10. data/Rakefile +18 -0
  11. data/heartland-retail.gemspec +17 -0
  12. data/lib/heartland-retail.rb +1 -0
  13. data/lib/heartland/client.rb +299 -0
  14. data/lib/heartland/client/body.rb +13 -0
  15. data/lib/heartland/client/collection.rb +121 -0
  16. data/lib/heartland/client/errors.rb +17 -0
  17. data/lib/heartland/client/resource.rb +215 -0
  18. data/lib/heartland/client/response.rb +84 -0
  19. data/lib/heartland/client/uri.rb +117 -0
  20. data/spec/heartland/client/body_spec.rb +31 -0
  21. data/spec/heartland/client/resource_spec.rb +318 -0
  22. data/spec/heartland/client/response_spec.rb +105 -0
  23. data/spec/heartland/client/uri_spec.rb +157 -0
  24. data/spec/heartland/client_spec.rb +361 -0
  25. data/spec/shared_client_context.rb +5 -0
  26. data/spec/spec_helper.rb +17 -0
  27. data/vendor/cache/addressable-2.2.8.gem +0 -0
  28. data/vendor/cache/codeclimate-test-reporter-1.0.8.gem +0 -0
  29. data/vendor/cache/coderay-1.1.0.gem +0 -0
  30. data/vendor/cache/coveralls-0.7.11.gem +0 -0
  31. data/vendor/cache/crack-0.4.2.gem +0 -0
  32. data/vendor/cache/diff-lcs-1.2.5.gem +0 -0
  33. data/vendor/cache/docile-1.1.5.gem +0 -0
  34. data/vendor/cache/faraday-0.17.3.gem +0 -0
  35. data/vendor/cache/hashie-4.1.0.gem +0 -0
  36. data/vendor/cache/json-2.3.0.gem +0 -0
  37. data/vendor/cache/method_source-0.8.2.gem +0 -0
  38. data/vendor/cache/mime-types-2.4.3.gem +0 -0
  39. data/vendor/cache/multi_json-1.11.0.gem +0 -0
  40. data/vendor/cache/multipart-post-2.1.1.gem +0 -0
  41. data/vendor/cache/netrc-0.10.3.gem +0 -0
  42. data/vendor/cache/pry-0.10.1.gem +0 -0
  43. data/vendor/cache/rake-10.4.2.gem +0 -0
  44. data/vendor/cache/rest-client-1.7.3.gem +0 -0
  45. data/vendor/cache/rspec-3.2.0.gem +0 -0
  46. data/vendor/cache/rspec-core-3.2.2.gem +0 -0
  47. data/vendor/cache/rspec-expectations-3.2.0.gem +0 -0
  48. data/vendor/cache/rspec-mocks-3.2.1.gem +0 -0
  49. data/vendor/cache/rspec-support-3.2.2.gem +0 -0
  50. data/vendor/cache/safe_yaml-1.0.4.gem +0 -0
  51. data/vendor/cache/simplecov-0.9.2.gem +0 -0
  52. data/vendor/cache/simplecov-html-0.9.0.gem +0 -0
  53. data/vendor/cache/slop-3.6.0.gem +0 -0
  54. data/vendor/cache/term-ansicolor-1.3.0.gem +0 -0
  55. data/vendor/cache/thor-0.19.1.gem +0 -0
  56. data/vendor/cache/tins-1.3.5.gem +0 -0
  57. data/vendor/cache/webmock-1.17.4.gem +0 -0
  58. metadata +140 -0
@@ -0,0 +1,17 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'heartland-retail'
3
+ s.version = '5.0.0'
4
+ s.platform = Gem::Platform::RUBY
5
+ s.authors = ['Jay Stotz', 'Derek Stotz']
6
+ s.summary = 'Heartland Retail API client library'
7
+
8
+ s.required_rubygems_version = '>= 1.3.6'
9
+
10
+ s.add_runtime_dependency 'faraday', '< 1.0'
11
+ s.add_runtime_dependency 'json', '>= 1.7.4'
12
+ s.add_runtime_dependency 'hashie'
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+
16
+ s.require_path = 'lib'
17
+ end
@@ -0,0 +1 @@
1
+ require_relative 'heartland/client'
@@ -0,0 +1,299 @@
1
+ require 'rubygems'
2
+ require 'faraday'
3
+ require 'json'
4
+ require 'logger'
5
+
6
+ require_relative 'client/errors'
7
+
8
+ ##
9
+ # HeartlandRetail namespace
10
+ module HeartlandRetail
11
+ ##
12
+ # The main point of interaction for the Heartland Retail Client library.
13
+ #
14
+ # Client code must successfully authenticate with the API via the {#auth}
15
+ # method before calling any HTTP methods or the API will return authorization
16
+ # errors.
17
+ #
18
+ # Provides direct access to the URI-oriented interface via the HTTP methods.
19
+ # Provides access to the URI-oriented interface via the {#[]} method.
20
+ class Client
21
+ ##
22
+ # Default number of records per page when iterating over collection resources
23
+ DEFAULT_PER_PAGE = 20
24
+
25
+ ##
26
+ # Default request timeout in seconds
27
+ DEFAULT_TIMEOUT = 60
28
+
29
+ ##
30
+ # Default connection timeout in seconds
31
+ DEFAULT_CONNECT_TIMEOUT = 10
32
+
33
+ ##
34
+ # @return [URI] The client's base URI
35
+ attr_reader :base_uri
36
+
37
+ ##
38
+ # @return [Faraday::Connection] Faraday's connection
39
+ attr_reader :connection
40
+
41
+ ##
42
+ # @param [String] base_uri Base URI
43
+ # @option opts [Boolean, String] :debug Pass true to debug to stdout. Pass a String to debug to given filename.
44
+ # @option opts [Boolean] :insecure Disable SSL certificate verification
45
+ # @option opts [String] :token Heartland Retail API Token
46
+ def initialize(base_uri, opts={})
47
+ @base_uri = URI.parse(base_uri)
48
+ @opts = opts
49
+ configure_connection!
50
+ end
51
+
52
+ ##
53
+ # Set to true to enable debugging to STDOUT or a string to write to the file
54
+ # at that path.
55
+ #
56
+ # @param [String, Boolean] debug
57
+ #
58
+ # @return [String, Boolean] The debug argument
59
+ def debug=(debug)
60
+ @opts[:debug] = debug
61
+ configure_connection!
62
+ end
63
+
64
+ ##
65
+ # @deprecated see {#initialize}.
66
+ # Passes the given credentials to the server, storing the session token on success.
67
+ #
68
+ # @raise [AuthFailed] If the credentials were invalid or the server returned an error
69
+ #
70
+ # @return [true]
71
+ #
72
+ # @option opts [String] :username Heartland Retail username
73
+ # @option opts [String] :password Heartland Retail password
74
+ def auth(opts={})
75
+ warn "[DEPRECATION] `auth` is deprecated. Please use `HeartlandRetail::Client.new '#{base_uri}', token: 'secret_token'` instead."
76
+
77
+ unless opts[:username] && opts[:password]
78
+ raise "Must specify :username and :password"
79
+ end
80
+ body = ::URI.encode_www_form \
81
+ :auth_key => opts[:username],
82
+ :password => opts[:password]
83
+ response = post '/auth/identity/callback', body,
84
+ 'Content-Type' => 'application/x-www-form-urlencoded'
85
+
86
+ if response.success?
87
+ @session_cookie = response.headers['set-cookie']
88
+ return true
89
+ else
90
+ raise AuthFailed, "Heartland Retail auth failed"
91
+ end
92
+ end
93
+
94
+ ##
95
+ # Performs a HEAD request against the given URI and returns the {Response}.
96
+ #
97
+ # @return [Response]
98
+ def head(uri, headers=false); make_request(:head, uri, headers); end
99
+
100
+ ##
101
+ # Performs a HEAD request against the given URI. Returns the {Response}
102
+ # on success and raises a {RequestFailed} on failure.
103
+ #
104
+ # @raise [RequestFailed] On error response
105
+ #
106
+ # @return [Response]
107
+ def head!(uri, headers=false); raise_on_fail head(uri, headers); end
108
+
109
+ ##
110
+ # Performs a GET request against the given URI and returns the {Response}.
111
+ #
112
+ # @return [Response]
113
+ def get(uri, headers=false); make_request(:get, uri, headers); end
114
+
115
+ ##
116
+ # Performs a GET request against the given URI. Returns the {Response}
117
+ # on success and raises a {RequestFailed} on failure.
118
+ #
119
+ # @raise [RequestFailed] On error response
120
+ #
121
+ # @return [Response]
122
+ def get!(uri, headers=false); raise_on_fail get(uri, headers); end
123
+
124
+ ##
125
+ # Performs a DELETE request against the given URI and returns the {Response}.
126
+ #
127
+ # @return [Response]
128
+ def delete(uri, headers=false); make_request(:delete, uri, headers); end
129
+
130
+ ##
131
+ # Performs a DELETE request against the given URI. Returns the {Response}
132
+ # on success and raises a {RequestFailed} on failure.
133
+ #
134
+ # @raise [RequestFailed] On error response
135
+ #
136
+ # @return [Response]
137
+ def delete!(uri, headers=false); raise_on_fail delete(uri, headers); end
138
+
139
+ ##
140
+ # Performs a PUT request against the given URI and returns the {Response}.
141
+ #
142
+ # @return [Response]
143
+ def put(uri, body, headers=false); make_request(:put, uri, headers, body); end
144
+
145
+ ##
146
+ # Performs a PUT request against the given URI. Returns the {Response}
147
+ # on success and raises a {RequestFailed} on failure.
148
+ #
149
+ # @raise [RequestFailed] On error response
150
+ #
151
+ # @return [Response]
152
+ def put!(uri, body, headers=false); raise_on_fail put(uri, body, headers); end
153
+
154
+ ##
155
+ # Performs a POST request against the given URI and returns the {Response}.
156
+ #
157
+ # @return [Response]
158
+ def post(uri, body, headers=false); make_request(:post, uri, headers, body); end
159
+
160
+ ##
161
+ # Performs a POST request against the given URI. Returns the {Response}
162
+ # on success and raises a {RequestFailed} on failure.
163
+ #
164
+ # @raise [RequestFailed] On error response
165
+ #
166
+ # @return [Response]
167
+ def post!(uri, body, headers=false); raise_on_fail post(uri, body, headers); end
168
+
169
+ ##
170
+ # Returns a Resource for the given URI path.
171
+ #
172
+ # @return [Resource]
173
+ def [](uri)
174
+ Resource.new(self, uri)
175
+ end
176
+
177
+ ##
178
+ # Iterates over each page of subordinate resources of the given collection
179
+ # resource URI and yields the {Response} to the block.
180
+ def each_page(uri)
181
+ uri = URI.parse(uri)
182
+ total_pages = nil
183
+ page = 1
184
+ uri.query_values = {'per_page' => DEFAULT_PER_PAGE}.merge(uri.query_values || {})
185
+ while total_pages.nil? or page <= total_pages
186
+ uri.merge_query_values! 'page' => page
187
+ response = get!(uri)
188
+ yield response
189
+ total_pages ||= response['pages']
190
+ page += 1
191
+ end
192
+ end
193
+
194
+ ##
195
+ # Iterates over each subordinate resource of the given collection resource
196
+ # URI and yields its representation to the given block.
197
+ def each(uri)
198
+ each_page(uri) do |page|
199
+ page['results'].each do |result|
200
+ yield result
201
+ end
202
+ end
203
+ end
204
+
205
+ ##
206
+ # Returns a count of subordinate resources of the given collection resource
207
+ # URI.
208
+ #
209
+ # @param [#to_s] uri
210
+ # @raise [RequestFailed] If the GET fails
211
+ # @return [Integer] The subordinate resource count
212
+ def count(uri)
213
+ uri = URI.parse(uri)
214
+ uri.merge_query_values! 'page' => 1, 'per_page' => 1
215
+ get!(uri)['total']
216
+ end
217
+
218
+ private
219
+
220
+ attr_reader :opts, :session_cookie
221
+
222
+ def prepare_request_body(body)
223
+ body.is_a?(Hash) ? JSON.dump(body) : body
224
+ end
225
+
226
+ def make_request(method, uri, headers=false, body=false)
227
+ response = connection.__send__( method, prepare_uri(uri)) do |request|
228
+ request.headers = headers unless headers === false
229
+ request.headers['Cookie'] = session_cookie if session_cookie
230
+
231
+ request.body = prepare_request_body(body) unless body === false
232
+ end
233
+
234
+ new_response(response)
235
+ end
236
+
237
+ def raise_on_fail(response)
238
+ if !response.success?
239
+ error = RequestFailed.new "Request failed with status: #{response.status}"
240
+ error.response = response
241
+ raise error
242
+ end
243
+ response
244
+ end
245
+
246
+ def prepare_uri(uri)
247
+ uri = URI.parse(uri)
248
+ uri.to_s
249
+ .gsub(/^#{base_uri.to_s}|^#{base_uri.path}/, '')
250
+ .gsub(/^\//, '')
251
+ end
252
+
253
+ def new_response(faraday_response)
254
+ Response.new faraday_response, self
255
+ end
256
+
257
+ def configure_connection!
258
+ @connection = Faraday.new
259
+
260
+ connection.url_prefix= base_uri.to_s
261
+
262
+ connection.headers['Content-Type'] = 'application/json'
263
+ connection.headers['Authorization'] = "Bearer #{opts[:token]}" if opts[:token]
264
+
265
+ connection.ssl[:verify] = false if opts.has_key?(:insecure)
266
+
267
+ connection.options.timeout = DEFAULT_TIMEOUT
268
+ connection.options.open_timeout = DEFAULT_CONNECT_TIMEOUT
269
+
270
+ if debug = opts[:debug]
271
+ connection.response :logger, debug_logger(debug), bodies: true
272
+ end
273
+ end
274
+
275
+ def debug_logger(debug)
276
+ Logger.new(debug == true ? STDOUT : debug)
277
+ end
278
+ end
279
+ end
280
+
281
+ ##
282
+ # Springboard namespace as alias of HeartlandRetail namespace for backwards compatability
283
+ module Springboard
284
+ include HeartlandRetail
285
+
286
+ ##
287
+ # HeartlandRetail::Client with added deprecation warning for Springboard namespace
288
+ class Client < HeartlandRetail::Client
289
+ def initialize(base_uri, opts={})
290
+ warn "[DEPRECATION] `Springboard::Client.new` is deprecated. Please use `HeartlandRetail::Client.new` instead."
291
+ super
292
+ end
293
+ end
294
+ end
295
+
296
+ require_relative 'client/resource'
297
+ require_relative 'client/response'
298
+ require_relative 'client/body'
299
+ require_relative 'client/uri'
@@ -0,0 +1,13 @@
1
+ require 'hashie'
2
+
3
+ module HeartlandRetail
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
+ disable_warnings if respond_to?(:disable_warnings)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,121 @@
1
+ module HeartlandRetail
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 Heartland Retail 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
+ # Returns a new resource with the given fields added to the query string as _only parameters.
84
+ #
85
+ # @example
86
+ # resource.only('id', :public_id)
87
+ #
88
+ # @param [#to_s] returns One or more fields
89
+ #
90
+ # @return [Resource]
91
+ def only(*fields)
92
+ query('_only' => fields)
93
+ end
94
+
95
+ ##
96
+ # Performs a request to get the first result of the first page of the
97
+ # collection and returns it.
98
+ #
99
+ # @raise [RequestFailed]
100
+ #
101
+ # @return [Body] The first entry in the response :results array
102
+ def first
103
+ response = query(:per_page => 1, :page => 1).get!
104
+ response[:results].first
105
+ end
106
+
107
+ ##
108
+ # Performs repeated GET requests to the resource and yields results to
109
+ # the given block as long as the response includes more results.
110
+ #
111
+ # @raise [RequestFailed]
112
+ def while_results(&block)
113
+ loop do
114
+ results = get![:results]
115
+ break if results.nil? || results.empty?
116
+ results.each(&block)
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end