supercast 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 108533a85e7a88356f68909d916b94c923b2e46c583d9790f233a6c8c449e43e
4
+ data.tar.gz: 927124213489bd07194bdf07bc5b796cd32e7d9fa7cb77cf1f3fc35693e1fb25
5
+ SHA512:
6
+ metadata.gz: 56bba6020c81f6780051619eb2ed7a6d39d05aa39cf62a89003b207e60cbeac8b9611bba45ab7b2641ee8080c7d9e2217956e3748be16ead33b620aff6c5d60f
7
+ data.tar.gz: 6e276d3b35ea0648ccdc85c00be5edb45c8a48f5fe664cde8fb0c1c95b07e10eaf93bb73e01de1b3bd111d09424a7345b6d1116eb53bfe740c51e4ab5d93e5e6
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require 'irb'
6
+ require 'irb/completion'
7
+
8
+ require "#{::File.dirname(__FILE__)}/../lib/supercast"
9
+
10
+ def reload!
11
+ puts 'Reloading ...'
12
+
13
+ load("#{File.expand_path('..', __dir__)}/lib/supercast.rb")
14
+
15
+ true
16
+ end
17
+
18
+ # Config IRB to enable --simple-prompt and auto indent
19
+ IRB.conf[:PROMPT_MODE] = :SIMPLE
20
+ IRB.conf[:AUTO_INDENT] = true
21
+
22
+ puts "Loaded gem 'supercast'"
23
+
24
+ IRB.start
@@ -0,0 +1,552 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supercast
4
+ # Client executes requests against the Supercast API and allows a user to
5
+ # recover both a resource a call returns as well as a response object that
6
+ # contains information on the HTTP call.
7
+ class Client
8
+ attr_accessor :conn
9
+
10
+ # Initializes a new Client. Expects a Faraday connection object, and
11
+ # uses a default connection unless one is passed.
12
+ def initialize(conn = nil)
13
+ self.conn = conn || self.class.default_conn
14
+ @system_profiler = SystemProfiler.new
15
+ end
16
+
17
+ def self.active_client
18
+ Thread.current[:supercast_client] || default_client
19
+ end
20
+
21
+ def self.default_client
22
+ Thread.current[:supercast_client_default_client] ||=
23
+ Client.new(default_conn)
24
+ end
25
+
26
+ # A default Faraday connection to be used when one isn't configured. This
27
+ # object should never be mutated, and instead instantiating your own
28
+ # connection and wrapping it in a Client object should be preferred.
29
+ def self.default_conn
30
+ # We're going to keep connections around so that we can take advantage
31
+ # of connection re-use, so make sure that we have a separate connection
32
+ # object per thread.
33
+ Thread.current[:supercast_client_default_conn] ||= begin
34
+ conn = Faraday.new do |builder|
35
+ builder.use Faraday::Request::Multipart
36
+ builder.use Faraday::Request::UrlEncoded
37
+ builder.use Faraday::Response::RaiseError
38
+
39
+ # Net::HTTP::Persistent doesn't seem to do well on Windows or JRuby,
40
+ # so fall back to default there.
41
+ if Gem.win_platform? || RUBY_PLATFORM == 'java'
42
+ builder.adapter :net_http
43
+ else
44
+ builder.adapter :net_http_persistent
45
+ end
46
+ end
47
+
48
+ conn.proxy = Supercast.proxy if Supercast.proxy
49
+
50
+ if Supercast.verify_ssl_certs
51
+ conn.ssl.verify = true
52
+ conn.ssl.cert_store = Supercast.ca_store
53
+ else
54
+ conn.ssl.verify = false
55
+
56
+ unless @verify_ssl_warned
57
+ @verify_ssl_warned = true
58
+
59
+ warn('WARNING: Running without SSL cert verification. ' \
60
+ 'You should never do this in production. ' \
61
+ 'Execute `Supercast.verify_ssl_certs = true` to enable ' \
62
+ 'verification.')
63
+ end
64
+ end
65
+
66
+ conn
67
+ end
68
+ end
69
+
70
+ # Checks if an error is a problem that we should retry on. This includes
71
+ # both socket errors that may represent an intermittent problem and some
72
+ # special HTTP statuses.
73
+ def self.should_retry?(error, num_retries)
74
+ return false if num_retries >= Supercast.max_network_retries
75
+
76
+ # Retry on timeout-related problems (either on open or read).
77
+ return true if error.is_a?(Faraday::TimeoutError)
78
+
79
+ # Destination refused the connection, the connection was reset, or a
80
+ # variety of other connection failures. This could occur from a single
81
+ # saturated server, so retry in case it's intermittent.
82
+ return true if error.is_a?(Faraday::ConnectionFailed)
83
+
84
+ false
85
+ end
86
+
87
+ def self.sleep_time(num_retries)
88
+ # Apply exponential backoff with initial_network_retry_delay on the
89
+ # number of num_retries so far as inputs. Do not allow the number to
90
+ # exceed max_network_retry_delay.
91
+ sleep_seconds = [
92
+ Supercast.initial_network_retry_delay * (2**(num_retries - 1)),
93
+ Supercast.max_network_retry_delay
94
+ ].min
95
+
96
+ # Apply some jitter by randomizing the value in the range of
97
+ # (sleep_seconds / 2) to (sleep_seconds).
98
+ sleep_seconds *= (0.5 * (1 + rand))
99
+
100
+ # But never sleep less than the base sleep seconds.
101
+ sleep_seconds = [Supercast.initial_network_retry_delay, sleep_seconds].max
102
+
103
+ sleep_seconds
104
+ end
105
+
106
+ # Executes the API call within the given block. Usage looks like:
107
+ #
108
+ # client = Client.new
109
+ # charge, resp = client.request { Episode.create }
110
+ #
111
+ def request
112
+ @last_response = nil
113
+ old_supercast_client = Thread.current[:supercast_client]
114
+ Thread.current[:supercast_client] = self
115
+
116
+ begin
117
+ res = yield
118
+ [res, @last_response]
119
+ ensure
120
+ Thread.current[:supercast_client] = old_supercast_client
121
+ end
122
+ end
123
+
124
+ def execute_request(method, path, api_base: nil, api_version: nil, api_key: nil, headers: {}, params: {}) # rubocop:disable Metrics/AbcSize Metrics/MethodLength
125
+ api_base ||= Supercast.api_base
126
+ api_version ||= Supercast.api_version
127
+ api_key ||= Supercast.api_key
128
+ params = Util.objects_to_ids(params)
129
+
130
+ check_api_key!(api_key)
131
+
132
+ body = nil
133
+ query_params = nil
134
+ case method.to_s.downcase.to_sym
135
+ when :get, :head, :delete
136
+ query_params = params
137
+ else
138
+ body = params
139
+ end
140
+
141
+ # This works around an edge case where we end up with both query
142
+ # parameters in `query_params` and query parameters that are appended
143
+ # onto the end of the given path. In this case, Faraday will silently
144
+ # discard the URL's parameters which may break a request.
145
+ #
146
+ # Here we decode any parameters that were added onto the end of a path
147
+ # and add them to `query_params` so that all parameters end up in one
148
+ # place and all of them are correctly included in the final request.
149
+ u = URI.parse(path)
150
+ unless u.query.nil?
151
+ query_params ||= {}
152
+ query_params = Hash[URI.decode_www_form(u.query)].merge(query_params)
153
+
154
+ # Reset the path minus any query parameters that were specified.
155
+ path = u.path
156
+ end
157
+
158
+ headers = request_headers(api_key, method).update(Util.normalize_headers(headers))
159
+ params_encoder = FaradaySupercastEncoder.new
160
+ url = api_url(path, api_base, api_version)
161
+
162
+ # stores information on the request we're about to make so that we don't
163
+ # have to pass as many parameters around for logging.
164
+ context = RequestLogContext.new
165
+ context.account = headers['Supercast-Account']
166
+ context.api_key = api_key
167
+ context.api_version = headers['Supercast-Version']
168
+ context.body = body ? params_encoder.encode(body) : nil
169
+ context.method = method
170
+ context.path = path
171
+ context.query_params = (params_encoder.encode(query_params) if query_params)
172
+
173
+ # note that both request body and query params will be passed through
174
+ # `FaradaySupercastEncoder`
175
+ http_resp = execute_request_with_rescues(api_base, context) do
176
+ conn.run_request(method, url, body, headers) do |req|
177
+ req.options.open_timeout = Supercast.open_timeout
178
+ req.options.params_encoder = params_encoder
179
+ req.options.timeout = Supercast.read_timeout
180
+ req.params = query_params unless query_params.nil?
181
+ end
182
+ end
183
+
184
+ begin
185
+ resp = Response.from_faraday_response(http_resp)
186
+ rescue JSON::ParserError
187
+ raise general_api_error(http_resp.status, http_resp.body)
188
+ end
189
+
190
+ # Allows Client#request to return a response object to a caller.
191
+ @last_response = resp
192
+ [resp, api_key]
193
+ end
194
+
195
+ # Used to workaround buggy behavior in Faraday: the library will try to
196
+ # reshape anything that we pass to `req.params` with one of its default
197
+ # encoders. I don't think this process is supposed to be lossy, but it is
198
+ # -- in particular when we send our integer-indexed maps (i.e. arrays),
199
+ # Faraday ends up stripping out the integer indexes.
200
+ #
201
+ # We work around the problem by implementing our own simplified encoder and
202
+ # telling Faraday to use that.
203
+ #
204
+ # The class also performs simple caching so that we don't have to encode
205
+ # parameters twice for every request (once to build the request and once
206
+ # for logging).
207
+ #
208
+ # When initialized with `multipart: true`, the encoder just inspects the
209
+ # hash instead to get a decent representation for logging. In the case of a
210
+ # multipart request, Faraday won't use the result of this encoder.
211
+ class FaradaySupercastEncoder
212
+ def initialize
213
+ @cache = {}
214
+ end
215
+
216
+ # This is quite subtle, but for a `multipart/form-data` request Faraday
217
+ # will throw away the result of this encoder and build its body.
218
+ def encode(hash)
219
+ @cache.fetch(hash) do |k|
220
+ @cache[k] = Util.encode_parameters(hash)
221
+ end
222
+ end
223
+
224
+ # We should never need to do this so it's not implemented.
225
+ def decode(_str)
226
+ raise NotImplementedError,
227
+ "#{self.class.name} does not implement #decode"
228
+ end
229
+ end
230
+
231
+ private
232
+
233
+ def api_url(url = '', api_base = nil, api_version = nil)
234
+ "#{api_base || Supercast.api_base}/#{api_version}#{url}"
235
+ end
236
+
237
+ def check_api_key!(api_key)
238
+ unless api_key
239
+ raise AuthenticationError, 'No API key provided. ' \
240
+ 'Set your API key using "Supercast.api_key = <API-KEY>". ' \
241
+ 'You can generate API keys from the Supercast web interface. ' \
242
+ 'See https://docs.supercast.tech/docs/access-tokens for details, or email ' \
243
+ 'support@supercast.com if you have any questions.'
244
+ end
245
+
246
+ return unless api_key =~ /\s/
247
+
248
+ raise AuthenticationError, 'Your API key is invalid, as it contains ' \
249
+ 'whitespace. (HINT: You can double-check your API key from the ' \
250
+ 'Supercast web interface. See https://docs.supercast.tech/docs/access-tokens for details, or ' \
251
+ 'email support@supercast.com if you have any questions.)'
252
+ end
253
+
254
+ def execute_request_with_rescues(api_base, context)
255
+ num_retries = 0
256
+ begin
257
+ request_start = Time.now
258
+ log_request(context, num_retries)
259
+ resp = yield
260
+ context = context.dup_from_response(resp)
261
+ log_response(context, request_start, resp.status, resp.body)
262
+
263
+ # We rescue all exceptions from a request so that we have an easy spot to
264
+ # implement our retry logic across the board. We'll re-raise if it's a
265
+ # type of exception that we didn't expect to handle.
266
+ rescue StandardError => e
267
+ # If we modify context we copy it into a new variable so as not to
268
+ # taint the original on a retry.
269
+ error_context = context
270
+
271
+ if e.respond_to?(:response) && e.response
272
+ error_context = context.dup_from_response(e.response)
273
+ log_response(error_context, request_start,
274
+ e.response[:status], e.response[:body])
275
+ else
276
+ log_response_error(error_context, request_start, e)
277
+ end
278
+
279
+ if self.class.should_retry?(e, num_retries)
280
+ num_retries += 1
281
+ sleep self.class.sleep_time(num_retries)
282
+ retry
283
+ end
284
+
285
+ case e
286
+ when Faraday::ClientError
287
+ if e.response
288
+ handle_error_response(e.response, error_context)
289
+ else
290
+ handle_network_error(e, error_context, num_retries, api_base)
291
+ end
292
+
293
+ # Only handle errors when we know we can do so, and re-raise otherwise.
294
+ # This should be pretty infrequent.
295
+ else
296
+ raise
297
+ end
298
+ end
299
+
300
+ resp
301
+ end
302
+
303
+ def general_api_error(status, body)
304
+ APIError.new("Invalid response object from API: #{body.inspect} " \
305
+ "(HTTP response code was #{status})",
306
+ http_status: status, http_body: body)
307
+ end
308
+
309
+ def handle_error_response(http_resp, context)
310
+ begin
311
+ resp = Response.from_faraday_hash(http_resp)
312
+ rescue StandardError
313
+ raise general_api_error(http_resp[:status], http_resp[:body])
314
+ end
315
+
316
+ error = specific_api_error(resp, context)
317
+
318
+ error.response = resp
319
+ raise(error)
320
+ end
321
+
322
+ def specific_api_error(resp, context)
323
+ Util.log_error('Supercast API error',
324
+ status: resp.http_status,
325
+ error_code: resp.http_status,
326
+ error_message: resp.data[:message],
327
+ idempotency_key: context.idempotency_key)
328
+
329
+ # The standard set of arguments that can be used to initialize most of
330
+ # the exceptions.
331
+ opts = {
332
+ http_body: resp.http_body,
333
+ http_headers: resp.http_headers,
334
+ http_status: resp.http_status,
335
+ json_body: resp.data,
336
+ code: resp.http_status
337
+ }
338
+
339
+ case resp.http_status
340
+ when 400, 404, 422
341
+ InvalidRequestError.new(resp.data[:message], opts)
342
+ when 401
343
+ AuthenticationError.new(resp.data[:message], opts)
344
+ when 403
345
+ PermissionError.new(resp.data[:message], opts)
346
+ when 429
347
+ RateLimitError.new(resp.data[:message], opts)
348
+ else
349
+ APIError.new(resp.data[:message], opts)
350
+ end
351
+ end
352
+
353
+ def handle_network_error(error, context, num_retries,
354
+ api_base = nil)
355
+ Util.log_error('Supercast network error',
356
+ error_message: error.message,
357
+ idempotency_key: context.idempotency_key)
358
+
359
+ case error
360
+ when Faraday::ConnectionFailed
361
+ message = 'Unexpected error communicating when trying to connect to ' \
362
+ 'Supercast. You may be seeing this message because your DNS is not ' \
363
+ 'working. To check, try running `host supercast.com` from the ' \
364
+ 'command line.'
365
+
366
+ when Faraday::SSLError
367
+ message = 'Could not establish a secure connection to Supercast, you ' \
368
+ 'may need to upgrade your OpenSSL version. To check, try running ' \
369
+ '`openssl s_client -connect api.supercast.com:443` from the command ' \
370
+ 'line.'
371
+
372
+ when Faraday::TimeoutError
373
+ api_base ||= Supercast.api_base
374
+ message = "Could not connect to Supercast (#{api_base}). " \
375
+ 'Please check your internet connection and try again. ' \
376
+ "If this problem persists, you should check Supercast's service " \
377
+ 'status at https://status.supercast.com, or let us know at ' \
378
+ 'support@supercast.com.'
379
+
380
+ else
381
+ message = 'Unexpected error communicating with Supercast. ' \
382
+ 'If this problem persists, let us know at support@supercast.com.'
383
+
384
+ end
385
+
386
+ message += " Request was retried #{num_retries} times." if num_retries.positive?
387
+
388
+ raise APIConnectionError,
389
+ message + "\n\n(Network error: #{error.message})"
390
+ end
391
+
392
+ def request_headers(api_key, method)
393
+ headers = {
394
+ 'User-Agent' => "Supercast RubyBindings/#{Supercast::VERSION}",
395
+ 'Authorization' => "Bearer #{api_key}",
396
+ 'Content-Type' => 'application/x-www-form-urlencoded'
397
+ }
398
+
399
+ # It is only safe to retry network failures on post and delete
400
+ # requests if we add an Idempotency-Key header
401
+ headers['Idempotency-Key'] ||= SecureRandom.uuid if %i[post delete].include?(method) && Supercast.max_network_retries.positive?
402
+
403
+ headers['Supercast-Version'] = Supercast.api_version if Supercast.api_version
404
+
405
+ user_agent = @system_profiler.user_agent
406
+ begin
407
+ headers.update(
408
+ 'X-Supercast-Client-User-Agent' => JSON.generate(user_agent)
409
+ )
410
+ rescue StandardError => e
411
+ headers.update(
412
+ 'X-Supercast-Client-Raw-User-Agent' => user_agent.inspect,
413
+ :error => "#{e} (#{e.class})"
414
+ )
415
+ end
416
+
417
+ headers
418
+ end
419
+
420
+ def log_request(context, num_retries)
421
+ Util.log_info('Request to Supercast API',
422
+ account: context.account,
423
+ api_version: context.api_version,
424
+ idempotency_key: context.idempotency_key,
425
+ method: context.method,
426
+ num_retries: num_retries,
427
+ path: context.path)
428
+ Util.log_debug('Request details',
429
+ body: context.body,
430
+ idempotency_key: context.idempotency_key,
431
+ query_params: context.query_params)
432
+ end
433
+
434
+ def log_response(context, request_start, status, body)
435
+ Util.log_info('Response from Supercast API',
436
+ account: context.account,
437
+ api_version: context.api_version,
438
+ elapsed: Time.now - request_start,
439
+ idempotency_key: context.idempotency_key,
440
+ method: context.method,
441
+ path: context.path,
442
+ status: status)
443
+ Util.log_debug('Response details',
444
+ body: body,
445
+ idempotency_key: context.idempotency_key)
446
+ end
447
+
448
+ def log_response_error(context, request_start, error)
449
+ Util.log_error('Request error',
450
+ elapsed: Time.now - request_start,
451
+ error_message: error.message,
452
+ idempotency_key: context.idempotency_key,
453
+ method: context.method,
454
+ path: context.path)
455
+ end
456
+
457
+ # RequestLogContext stores information about a request that's begin made so
458
+ # that we can log certain information. It's useful because it means that we
459
+ # don't have to pass around as many parameters.
460
+ class RequestLogContext
461
+ attr_accessor :body
462
+ attr_accessor :account
463
+ attr_accessor :api_key
464
+ attr_accessor :api_version
465
+ attr_accessor :idempotency_key
466
+ attr_accessor :method
467
+ attr_accessor :path
468
+ attr_accessor :query_params
469
+
470
+ # The idea with this method is that we might want to update some of
471
+ # context information because a response that we've received from the API
472
+ # contains information that's more authoritative than what we started
473
+ # with for a request. For example, we should trust whatever came back in
474
+ # a `Supercast-Version` header beyond what configuration information that we
475
+ # might have had available.
476
+ def dup_from_response(resp)
477
+ return self if resp.nil?
478
+
479
+ # Faraday's API is a little unusual. Normally it'll produce a response
480
+ # object with a `headers` method, but on error what it puts into
481
+ # `e.response` is an untyped `Hash`.
482
+ headers = if resp.is_a?(Faraday::Response)
483
+ resp.headers
484
+ else
485
+ resp[:headers]
486
+ end
487
+
488
+ context = dup
489
+ context.account = headers['Supercast-Account']
490
+ context.api_version = headers['Supercast-Version']
491
+ context.idempotency_key = headers['Idempotency-Key']
492
+ context
493
+ end
494
+ end
495
+
496
+ # SystemProfiler extracts information about the system that we're running
497
+ # in so that we can generate a rich user agent header to help debug
498
+ # integrations.
499
+ class SystemProfiler
500
+ def self.uname
501
+ if ::File.exist?('/proc/version')
502
+ ::File.read('/proc/version').strip
503
+ else
504
+ case RbConfig::CONFIG['host_os']
505
+ when /linux|darwin|bsd|sunos|solaris|cygwin/i
506
+ uname_from_system
507
+ when /mswin|mingw/i
508
+ uname_from_system_ver
509
+ else
510
+ 'unknown platform'
511
+ end
512
+ end
513
+ end
514
+
515
+ def self.uname_from_system
516
+ (`uname -a 2>/dev/null` || '').strip
517
+ rescue Errno::ENOENT
518
+ 'uname executable not found'
519
+ rescue Errno::ENOMEM # couldn't create subprocess
520
+ 'uname lookup failed'
521
+ end
522
+
523
+ def self.uname_from_system_ver
524
+ (`ver` || '').strip
525
+ rescue Errno::ENOENT
526
+ 'ver executable not found'
527
+ rescue Errno::ENOMEM # couldn't create subprocess
528
+ 'uname lookup failed'
529
+ end
530
+
531
+ def initialize
532
+ @uname = self.class.uname
533
+ end
534
+
535
+ def user_agent
536
+ lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} " \
537
+ "(#{RUBY_RELEASE_DATE})"
538
+
539
+ {
540
+ bindings_version: Supercast::VERSION,
541
+ lang: 'ruby',
542
+ lang_version: lang_version,
543
+ platform: RUBY_PLATFORM,
544
+ engine: defined?(RUBY_ENGINE) ? RUBY_ENGINE : '',
545
+ publisher: 'supercast',
546
+ uname: @uname,
547
+ hostname: Socket.gethostname
548
+ }.delete_if { |_k, v| v.nil? }
549
+ end
550
+ end
551
+ end
552
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supercast
4
+ class DataList < DataObject
5
+ include Enumerable
6
+ include Supercast::Operations::List
7
+ include Supercast::Operations::Request
8
+ include Supercast::Operations::Create
9
+
10
+ OBJECT_NAME = 'list'
11
+
12
+ # This accessor allows a `DataList` to inherit various filters that were
13
+ # given to a predecessor. This allows for things like consistent limits,
14
+ # expansions, and predicates as a user pages through resources.
15
+ attr_accessor :filters
16
+
17
+ # An empty list object. This is returned from +next+ when we know that
18
+ # there isn't a next page in order to replicate the behavior of the API
19
+ # when it attempts to return a page beyond the last.
20
+ def self.empty_list(opts = {})
21
+ DataList.construct_from({ data: [] }, opts)
22
+ end
23
+
24
+ def initialize(*args)
25
+ super
26
+ self.filters = {}
27
+ end
28
+
29
+ def [](key)
30
+ case key
31
+ when String, Symbol
32
+ super
33
+ else
34
+ raise ArgumentError,
35
+ "You tried to access the #{key.inspect} index, but DataList " \
36
+ 'types only support String keys. (HINT: List calls return an ' \
37
+ "object with a 'data' (which is the data array). You likely " \
38
+ "want to call #data[#{key.inspect}])"
39
+ end
40
+ end
41
+
42
+ # Iterates through each resource in the page represented by the current
43
+ # `DataList`.
44
+ #
45
+ # Note that this method makes no effort to fetch a new page when it gets to
46
+ # the end of the current page's resources. See also +auto_paging_each+.
47
+ def each(&blk)
48
+ data.each(&blk)
49
+ end
50
+
51
+ # Iterates through each resource in all pages, making additional fetches to
52
+ # the API as necessary.
53
+ #
54
+ # Note that this method will make as many API calls as necessary to fetch
55
+ # all resources. For more granular control, please see +each+ and
56
+ # +next_page+.
57
+ def auto_paging_each(&blk)
58
+ return enum_for(:auto_paging_each) unless block_given?
59
+
60
+ page = self
61
+
62
+ loop do
63
+ page.each(&blk)
64
+ page = page.next_page
65
+ break if page.empty?
66
+ end
67
+ end
68
+
69
+ # Returns true if the page object contains no elements.
70
+ def empty?
71
+ data.empty?
72
+ end
73
+
74
+ def retrieve(id, opts = {})
75
+ id, retrieve_params = Util.normalize_id(id)
76
+ resp, opts = request(:get, "#{resource_url}/#{CGI.escape(id)}",
77
+ retrieve_params, opts)
78
+ Util.convert_to_supercast_object(resp.data, opts)
79
+ end
80
+
81
+ # Fetches the next page in the resource list (if there is one).
82
+ #
83
+ # This method will try to respect the limit of the current page. If none
84
+ # was given, the default limit will be fetched again.
85
+ def next_page(params = {}, opts = {})
86
+ return self.class.empty_list(opts) unless defined?(page) && defined?(per_page) && defined?(total) && page * per_page < total
87
+
88
+ params = filters.merge(page: page + 1).merge(params)
89
+
90
+ list(params, opts)
91
+ end
92
+
93
+ # Fetches the previous page in the resource list (if there is one).
94
+ #
95
+ # This method will try to respect the limit of the current page. If none
96
+ # was given, the default limit will be fetched again.
97
+ def previous_page(params = {}, opts = {})
98
+ return self.class.empty_list(opts) unless page && page > 1
99
+
100
+ params = filters.merge(page: page - 1).merge(params)
101
+
102
+ list(params, opts)
103
+ end
104
+
105
+ def resource_url
106
+ url ||
107
+ raise(ArgumentError, "List object does not contain a 'url' field.")
108
+ end
109
+ end
110
+ end