supercast 0.0.2

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 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