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.
@@ -0,0 +1,410 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supercast
4
+ class DataObject
5
+ include Enumerable
6
+
7
+ @@permanent_attributes = Set.new([:id]) # rubocop:disable Style/ClassVars
8
+
9
+ # The default :id method is deprecated and isn't useful to us
10
+ undef :id if method_defined?(:id)
11
+
12
+ def initialize(id = nil, opts = {})
13
+ id, @retrieve_params = Util.normalize_id(id)
14
+ @opts = Util.normalize_opts(opts)
15
+ @original_values = {}
16
+ @values = {}
17
+ # This really belongs in Resource, but not putting it there allows us
18
+ # to have a unified inspect method
19
+ @unsaved_values = Set.new
20
+ @transient_values = Set.new
21
+ @values[:id] = id if id
22
+ end
23
+
24
+ def self.construct_from(values, opts = {})
25
+ values = Supercast::Util.symbolize_names(values)
26
+
27
+ # work around protected #initialize_from for now
28
+ new(values[:id]).send(:initialize_from, values, opts)
29
+ end
30
+
31
+ # Determines the equality of two Supercast objects. Supercast objects are
32
+ # considered to be equal if they have the same set of values and each one
33
+ # of those values is the same.
34
+ def ==(other)
35
+ other.is_a?(DataObject) &&
36
+ @values == other.instance_variable_get(:@values)
37
+ end
38
+
39
+ # Hash equality. As with `#==`, we consider two equivalent Supercast objects
40
+ # equal.
41
+ def eql?(other)
42
+ # Defer to the implementation on `#==`.
43
+ self == other
44
+ end
45
+
46
+ # As with equality in `#==` and `#eql?`, we hash two Supercast objects to the
47
+ # same value if they're equivalent objects.
48
+ def hash
49
+ @values.hash
50
+ end
51
+
52
+ def to_s(*_args)
53
+ JSON.pretty_generate(to_hash)
54
+ end
55
+
56
+ def inspect
57
+ id_string = respond_to?(:id) && !id.nil? ? " id=#{id}" : ''
58
+ "#<#{self.class}:0x#{object_id.to_s(16)}#{id_string}> JSON: " +
59
+ JSON.pretty_generate(@values)
60
+ end
61
+
62
+ # Mass assigns attributes on the model.
63
+ #
64
+ # This is a version of +update_attributes+ that takes some extra options
65
+ # for internal use.
66
+ #
67
+ # ==== Attributes
68
+ #
69
+ # * +values+ - Hash of values to use to update the current attributes of
70
+ # the object.
71
+ # * +opts+ - Options for +DataObject+ like an API key that will be reused
72
+ # on subsequent API calls.
73
+ #
74
+ # ==== Options
75
+ #
76
+ # * +:dirty+ - Whether values should be initiated as "dirty" (unsaved) and
77
+ # which applies only to new DataObjects being initiated under this
78
+ # DataObject. Defaults to true.
79
+ def update_attributes(values, opts = {}, dirty: true)
80
+ values.each do |k, v|
81
+ add_accessors([k], values) unless metaclass.method_defined?(k.to_sym)
82
+ @values[k] = Util.convert_to_supercast_object(v, opts)
83
+ dirty_value!(@values[k]) if dirty
84
+ @unsaved_values.add(k)
85
+ end
86
+ end
87
+
88
+ def [](key)
89
+ @values[key.to_sym]
90
+ end
91
+
92
+ def []=(key, value)
93
+ send(:"#{key}=", value)
94
+ end
95
+
96
+ def keys
97
+ @values.keys
98
+ end
99
+
100
+ def values
101
+ @values.values
102
+ end
103
+
104
+ def to_json(_opts)
105
+ JSON.generate(@values)
106
+ end
107
+
108
+ def as_json(*opts)
109
+ @values.as_json(*opts)
110
+ end
111
+
112
+ def to_hash
113
+ maybe_to_hash = lambda do |value|
114
+ value&.respond_to?(:to_hash) ? value.to_hash : value
115
+ end
116
+
117
+ @values.each_with_object({}) do |(key, value), acc|
118
+ acc[key] = case value
119
+ when Array
120
+ value.map(&maybe_to_hash)
121
+ else
122
+ maybe_to_hash.call(value)
123
+ end
124
+ end
125
+ end
126
+
127
+ def each(&blk)
128
+ @values.each(&blk)
129
+ end
130
+
131
+ # Sets all keys within the DataObject as unsaved so that they will be
132
+ # included with an update when #serialize_params is called.
133
+ def dirty!
134
+ @unsaved_values = Set.new(@values.keys)
135
+
136
+ @values.each_value do |v|
137
+ dirty_value!(v)
138
+ end
139
+ end
140
+
141
+ # Implements custom encoding for Ruby's Marshal. The data produced by this
142
+ # method should be comprehendable by #marshal_load.
143
+ #
144
+ # This allows us to remove certain features that cannot or should not be
145
+ # serialized.
146
+ def marshal_dump
147
+ # The Client instance in @opts is not serializable and is not
148
+ # really a property of the DataObject, so we exclude it when
149
+ # dumping
150
+ opts = @opts.clone
151
+ opts.delete(:client)
152
+ [@values, opts]
153
+ end
154
+
155
+ # Implements custom decoding for Ruby's Marshal. Consumes data that's
156
+ # produced by #marshal_dump.
157
+ def marshal_load(data)
158
+ values, opts = data
159
+ initialize(values[:id])
160
+ initialize_from(values, opts)
161
+ end
162
+
163
+ def serialize_params(options = {})
164
+ update_hash = {}
165
+
166
+ @values.each do |k, _v|
167
+ # There are a few reasons that we may want to add in a parameter for
168
+ # update:
169
+ #
170
+ # 1. The `force` option has been set.
171
+ # 2. We know that it was modified.
172
+ #
173
+ unsaved = @unsaved_values.include?(k)
174
+ next unless options[:force] || unsaved
175
+
176
+ update_hash[k.to_sym] = serialize_params_value(
177
+ @values[k], @original_values[k], unsaved, options[:force], key: k
178
+ )
179
+ end
180
+
181
+ # a `nil` that makes it out of `#serialize_params_value` signals an empty
182
+ # value that we shouldn't appear in the serialized form of the object
183
+ update_hash.reject! { |_, v| v.nil? }
184
+
185
+ update_hash
186
+ end
187
+
188
+ # A protected field is one that doesn't get an accessor assigned to it
189
+ # (i.e. `obj.public = ...`) and one which is not allowed to be updated via
190
+ # the class level `Model.update(id, { ... })`.
191
+ def self.protected_fields
192
+ []
193
+ end
194
+
195
+ protected
196
+
197
+ def metaclass
198
+ class << self; self; end
199
+ end
200
+
201
+ def remove_accessors(keys)
202
+ # not available in the #instance_eval below
203
+ protected_fields = self.class.protected_fields
204
+
205
+ metaclass.instance_eval do
206
+ keys.each do |k|
207
+ next if protected_fields.include?(k)
208
+ next if @@permanent_attributes.include?(k)
209
+
210
+ # Remove methods for the accessor's reader and writer.
211
+ [k, :"#{k}=", :"#{k}?"].each do |method_name|
212
+ next unless method_defined?(method_name)
213
+
214
+ begin
215
+ remove_method(method_name)
216
+ rescue NameError
217
+ warn("WARNING: Unable to remove method `#{method_name}`; " \
218
+ "if custom, please consider renaming to a name that doesn't " \
219
+ 'collide with an API property name.')
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
225
+
226
+ def add_accessors(keys, values)
227
+ # not available in the #instance_eval below
228
+ protected_fields = self.class.protected_fields
229
+
230
+ metaclass.instance_eval do
231
+ keys.each do |k|
232
+ next if protected_fields.include?(k)
233
+ next if @@permanent_attributes.include?(k)
234
+
235
+ if k == :method
236
+ # Object#method is a built-in Ruby method that accepts a symbol
237
+ # and returns the corresponding Method object. Because the API may
238
+ # also use `method` as a field name, we check the arity of *args
239
+ # to decide whether to act as a getter or call the parent method.
240
+ define_method(k) { |*args| args.empty? ? @values[k] : super(*args) }
241
+ else
242
+ define_method(k) { @values[k] }
243
+ end
244
+
245
+ define_method(:"#{k}=") do |v|
246
+ if v == ''
247
+ raise ArgumentError, "You cannot set #{k} to an empty string. " \
248
+ 'We interpret empty strings as nil in requests. ' \
249
+ "You may set (object).#{k} = nil to delete the property."
250
+ end
251
+ @values[k] = Util.convert_to_supercast_object(v, @opts)
252
+ dirty_value!(@values[k])
253
+ @unsaved_values.add(k)
254
+ end
255
+
256
+ define_method(:"#{k}?") { @values[k] } if [FalseClass, TrueClass].include?(values[k].class)
257
+ end
258
+ end
259
+ end
260
+
261
+ # Disabling the cop because it's confused by the fact that the methods are
262
+ # protected, but we do define `#respond_to_missing?` just below. Hopefully
263
+ # this is fixed in more recent Rubocop versions.
264
+ def method_missing(name, *args)
265
+ # TODO: only allow setting in updateable classes.
266
+ if name.to_s.end_with?('=')
267
+ attr = name.to_s[0...-1].to_sym
268
+
269
+ # Pull out the assigned value. This is only used in the case of a
270
+ # boolean value to add a question mark accessor (i.e. `foo?`) for
271
+ # convenience.
272
+ val = args.first
273
+
274
+ # the second argument is only required when adding boolean accessors
275
+ add_accessors([attr], attr => val)
276
+
277
+ begin
278
+ mth = method(name)
279
+ rescue NameError
280
+ raise NoMethodError,
281
+ "Cannot set #{attr} on this object. HINT: you can't set: " \
282
+ "#{@@permanent_attributes.to_a.join(', ')}"
283
+ end
284
+ return mth.call(args[0])
285
+ elsif @values.key?(name)
286
+ return @values[name]
287
+ end
288
+
289
+ begin
290
+ super
291
+ rescue NoMethodError => e
292
+ # If we notice the accessed name if our set of transient values we can
293
+ # give the user a slightly more helpful error message. If not, just
294
+ # raise right away.
295
+ raise unless @transient_values.include?(name)
296
+
297
+ raise NoMethodError,
298
+ e.message + ". HINT: The '#{name}' attribute was set in the " \
299
+ 'past, however. It was then wiped when refreshing the object ' \
300
+ "with the result returned by Supercast's API, probably as a " \
301
+ 'result of a save(). The attributes currently available on ' \
302
+ "this object are: #{@values.keys.join(', ')}"
303
+ end
304
+ end
305
+
306
+ def respond_to_missing?(symbol, include_private = false)
307
+ @values&.key?(symbol) || super
308
+ end
309
+
310
+ # Re-initializes the object based on a hash of values (usually one that's
311
+ # come back from an API call). Adds or removes value accessors as necessary
312
+ # and updates the state of internal data.
313
+ #
314
+ # Protected on purpose! Please do not expose.
315
+ #
316
+ # ==== Options
317
+ #
318
+ # * +:values:+ Hash used to update accessors and values.
319
+ # * +:opts:+ Options for DataObject like an API key.
320
+ def initialize_from(values, opts)
321
+ @opts = Util.normalize_opts(opts)
322
+
323
+ # the `#send` is here so that we can keep this method private
324
+ @original_values = self.class.send(:deep_copy, values)
325
+
326
+ removed = Set.new(@values.keys - values.keys)
327
+ added = Set.new(values.keys - @values.keys)
328
+
329
+ remove_accessors(removed)
330
+ add_accessors(added, values)
331
+
332
+ removed.each do |k|
333
+ @values.delete(k)
334
+ @transient_values.add(k)
335
+ @unsaved_values.delete(k)
336
+ end
337
+
338
+ update_attributes(values, opts, dirty: false)
339
+
340
+ values.each_key do |k|
341
+ @transient_values.delete(k)
342
+ @unsaved_values.delete(k)
343
+ end
344
+
345
+ self
346
+ end
347
+
348
+ def serialize_params_value(value, original, _unsaved, force)
349
+ if value.nil?
350
+ ''
351
+ elsif value.is_a?(Array)
352
+ update = value.map { |v| serialize_params_value(v, nil, true, force) }
353
+
354
+ # This prevents an array that's unchanged from being resent.
355
+ update if update != serialize_params_value(original, nil, true, force)
356
+
357
+ # Handle a Hash for now, but in the long run we should be able to
358
+ # eliminate all places where hashes are stored as values internally by
359
+ # making sure any time one is set, we convert it to a DataObject. This
360
+ # will simplify our model by making data within an object more
361
+ # consistent.
362
+ #
363
+ # For now, you can still run into a hash if someone appends one to an
364
+ # existing array being held by a DataObject. This could happen for
365
+ # example by appending a new hash onto `additional_owners` for an
366
+ # account.
367
+ elsif value.is_a?(Hash)
368
+ Util.convert_to_supercast_object(value, @opts).serialize_params
369
+ elsif value.is_a?(DataObject)
370
+ value.serialize_params(force: force)
371
+ else
372
+ value
373
+ end
374
+ end
375
+
376
+ # Produces a deep copy of the given object including support for arrays,
377
+ # hashes, and DataObjects.
378
+ private_class_method def self.deep_copy(obj)
379
+ case obj
380
+ when Array
381
+ obj.map { |e| deep_copy(e) }
382
+ when Hash
383
+ obj.each_with_object({}) do |(k, v), copy|
384
+ copy[k] = deep_copy(v)
385
+ copy
386
+ end
387
+ when DataObject
388
+ obj.class.construct_from(
389
+ deep_copy(obj.instance_variable_get(:@values)),
390
+ obj.instance_variable_get(:@opts).select do |k, _v|
391
+ Util::OPTS_COPYABLE.include?(k)
392
+ end
393
+ )
394
+ else
395
+ obj
396
+ end
397
+ end
398
+
399
+ private
400
+
401
+ def dirty_value!(value)
402
+ case value
403
+ when Array
404
+ value.map { |v| dirty_value!(v) }
405
+ when DataObject
406
+ value.dirty!
407
+ end
408
+ end
409
+ end
410
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supercast
4
+ module DataTypes
5
+ def self.object_names_to_classes
6
+ {
7
+ # data structures
8
+ DataList::OBJECT_NAME => DataList,
9
+
10
+ # business objects
11
+ Channel::OBJECT_NAME => Channel,
12
+ Creator::OBJECT_NAME => Creator,
13
+ Episode::OBJECT_NAME => Episode,
14
+ Invite::OBJECT_NAME => Invite,
15
+ Role::OBJECT_NAME => Role,
16
+ Subscriber::OBJECT_NAME => Subscriber,
17
+ UsageAlert::OBJECT_NAME => UsageAlert
18
+ }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supercast
4
+ # SupercastError is the base error from which all other more specific Supercast
5
+ # errors derive.
6
+ class SupercastError < StandardError
7
+ attr_reader :message
8
+
9
+ # Response contains a SupercastResponse object that has some basic information
10
+ # about the response that conveyed the error.
11
+ attr_accessor :response
12
+
13
+ attr_reader :code
14
+ attr_reader :http_body
15
+ attr_reader :http_headers
16
+ attr_reader :http_status
17
+ attr_reader :json_body # equivalent to #data
18
+
19
+ # Initializes a SupercastError.
20
+ def initialize(message = nil, http_status: nil, http_body: nil, json_body: nil, http_headers: nil, code: nil)
21
+ @message = message
22
+ @http_status = http_status
23
+ @http_body = http_body
24
+ @http_headers = http_headers || {}
25
+ @json_body = json_body
26
+ @code = code
27
+ end
28
+
29
+ def to_s
30
+ status_string = @http_status.nil? ? '' : "(Status #{@http_status}) "
31
+ "#{status_string}#{@message}"
32
+ end
33
+ end
34
+
35
+ # AuthenticationError is raised when invalid credentials are used to connect
36
+ # to Supercast's servers.
37
+ class AuthenticationError < SupercastError
38
+ end
39
+
40
+ # APIConnectionError is raised in the event that the SDK can't connect to
41
+ # Supercast's servers. That can be for a variety of different reasons such as a
42
+ # downed network
43
+ class APIConnectionError < SupercastError
44
+ end
45
+
46
+ # APIError is a generic error that may be raised in cases where none of the
47
+ # other named errors cover the problem. It could also be raised in the case
48
+ # that a new error has been introduced in the API, but this version of the
49
+ # Ruby SDK doesn't know how to handle it.
50
+ class APIError < SupercastError
51
+ end
52
+
53
+ # InvalidRequestError is raised when a request is initiated with invalid
54
+ # parameters.
55
+ class InvalidRequestError < SupercastError
56
+ def initialize(message, http_status: nil, http_body: nil, json_body: nil, http_headers: nil, code: nil)
57
+ super(message, http_status: http_status, http_body: http_body,
58
+ json_body: json_body, http_headers: http_headers,
59
+ code: code)
60
+ end
61
+ end
62
+
63
+ # PermissionError is raised in cases where access was attempted on a resource
64
+ # that wasn't allowed.
65
+ class PermissionError < SupercastError
66
+ end
67
+
68
+ # RateLimitError is raised in cases where an account is putting too much load
69
+ # on Supercast's API servers (usually by performing too many requests). Please
70
+ # back off on request rate.
71
+ class RateLimitError < SupercastError
72
+ end
73
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supercast
4
+ module Operations
5
+ module Create
6
+ # Creates an API resource.
7
+ #
8
+ # ==== Attributes
9
+ #
10
+ # * +params+ - A hash of parameters to pass to the API
11
+ # * +opts+ - A Hash of additional options (separate from the params /
12
+ # object values) to be added to the request.
13
+ def create(params = {}, opts = {})
14
+ resp, opts = request(:post, resource_url, Hash[object_name => params], opts)
15
+ Util.convert_to_supercast_object(resp.data, opts)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supercast
4
+ module Operations
5
+ module Destroy
6
+ module ClassMethods
7
+ # Deletes an API resource
8
+ #
9
+ # Deletes the identified resource with the passed in parameters.
10
+ #
11
+ # ==== Attributes
12
+ #
13
+ # * +id+ - ID of the resource to delete.
14
+ # * +params+ - A hash of parameters to pass to the API
15
+ # * +opts+ - A Hash of additional options (separate from the params /
16
+ # object values) to be added to the request.
17
+ def destroy(id, params = {}, opts = {})
18
+ request(:delete, "#{resource_url}/#{id}", params, opts)
19
+ true
20
+ end
21
+ end
22
+
23
+ # Deletes an API resource instance
24
+ #
25
+ # Deletes the instance resource with the passed in parameters.
26
+ #
27
+ # ==== Attributes
28
+ #
29
+ # * +params+ - A hash of parameters to pass to the API
30
+ # * +opts+ - A Hash of additional options (separate from the params /
31
+ # object values) to be added to the request.
32
+ def destroy(params = {}, opts = {})
33
+ request(:delete, resource_url, params, opts)
34
+ true
35
+ end
36
+
37
+ def self.included(base)
38
+ base.extend(ClassMethods)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supercast
4
+ module Operations
5
+ module List
6
+ # Lists an API resource
7
+ #
8
+ # ==== Attributes
9
+ #
10
+ # * +filters+ - A hash of filters to pass to the API
11
+ # * +opts+ - A Hash of additional options (separate from the params /
12
+ # object values) to be added to the request.
13
+ def list(filters = {}, opts = {})
14
+ opts = Util.normalize_opts(opts)
15
+
16
+ resp, opts = request(:get, resource_url, filters, opts)
17
+ obj = DataList.construct_from({
18
+ data: resp.data,
19
+ page: resp.http_headers['x-page'].to_i,
20
+ per_page: resp.http_headers['x-per-page'].to_i,
21
+ total: resp.http_headers['x-total'].to_i
22
+ }, opts)
23
+
24
+ # set filters so that we can fetch the same limit, expansions, and
25
+ # predicates when accessing the next and previous pages
26
+ #
27
+ # just for general cleanliness, remove any paging options
28
+ obj.filters = filters.dup
29
+ obj.filters.delete(:ending_before)
30
+ obj.filters.delete(:starting_after)
31
+
32
+ obj
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supercast
4
+ module Operations
5
+ module Request
6
+ module ClassMethods
7
+ # Invokes an HTTP request via the Supercast Client for
8
+ # manipulating an API resource.
9
+ #
10
+ # ==== Attributes
11
+ #
12
+ # * +method+ - A symbol for the HTTP verb to use in the request
13
+ # * +url+ - A string which dictates what API endpoint URL to hit
14
+ # * +params+ - A hash of parameters to pass to the API
15
+ # * +opts+ - A Hash of additional options (separate from the params /
16
+ # object values) to be added to the request.
17
+ def request(method, url, params = {}, opts = {})
18
+ warn_on_opts_in_params(params)
19
+
20
+ opts = Util.normalize_opts(opts)
21
+ opts[:client] ||= Client.active_client
22
+
23
+ headers = opts.clone
24
+ api_key = headers.delete(:api_key)
25
+ api_base = headers.delete(:api_base)
26
+ client = headers.delete(:client)
27
+ # Assume all remaining opts must be headers
28
+
29
+ resp, opts[:api_key] = client.execute_request(
30
+ method, url,
31
+ api_base: api_base, api_key: api_key,
32
+ headers: headers, params: params
33
+ )
34
+
35
+ # Hash#select returns an array before 1.9
36
+ opts_to_persist = {}
37
+ opts.each do |k, v|
38
+ opts_to_persist[k] = v if Util::OPTS_PERSISTABLE.include?(k)
39
+ end
40
+
41
+ [resp, opts_to_persist]
42
+ end
43
+
44
+ private
45
+
46
+ def warn_on_opts_in_params(params)
47
+ Util::OPTS_USER_SPECIFIED.each do |opt|
48
+ warn("WARNING: #{opt} should be in opts instead of params.") if params.key?(opt)
49
+ end
50
+ end
51
+ end
52
+
53
+ def self.included(base)
54
+ base.extend(ClassMethods)
55
+ end
56
+
57
+ protected
58
+
59
+ def request(method, url, params = {}, opts = {})
60
+ opts = @opts.merge(Util.normalize_opts(opts))
61
+ self.class.request(method, url, params, opts)
62
+ end
63
+ end
64
+ end
65
+ end