x-resources 1.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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +9 -0
  3. data/CHANGELOG.md +254 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +148 -0
  6. data/lib/x/resources/abstract_class.rb +29 -0
  7. data/lib/x/resources/actions/direct_messages.rb +99 -0
  8. data/lib/x/resources/actions/engagement.rb +95 -0
  9. data/lib/x/resources/actions/lists.rb +126 -0
  10. data/lib/x/resources/actions/posts.rb +77 -0
  11. data/lib/x/resources/actions/relationships.rb +91 -0
  12. data/lib/x/resources/actions.rb +22 -0
  13. data/lib/x/resources/api.rb +40 -0
  14. data/lib/x/resources/attributes.rb +203 -0
  15. data/lib/x/resources/batch.rb +43 -0
  16. data/lib/x/resources/batch_finders.rb +185 -0
  17. data/lib/x/resources/bookmark_folder.rb +23 -0
  18. data/lib/x/resources/community.rb +137 -0
  19. data/lib/x/resources/cursor.rb +493 -0
  20. data/lib/x/resources/direct_message.rb +325 -0
  21. data/lib/x/resources/direct_message_conversations.rb +147 -0
  22. data/lib/x/resources/errors.rb +104 -0
  23. data/lib/x/resources/finders.rb +255 -0
  24. data/lib/x/resources/identity.rb +65 -0
  25. data/lib/x/resources/includes.rb +216 -0
  26. data/lib/x/resources/list.rb +336 -0
  27. data/lib/x/resources/lookups/communities.rb +56 -0
  28. data/lib/x/resources/lookups/direct_messages.rb +86 -0
  29. data/lib/x/resources/lookups/lists.rb +44 -0
  30. data/lib/x/resources/lookups/media.rb +72 -0
  31. data/lib/x/resources/lookups/posts.rb +198 -0
  32. data/lib/x/resources/lookups/spaces.rb +87 -0
  33. data/lib/x/resources/lookups/trends.rb +38 -0
  34. data/lib/x/resources/lookups/users.rb +221 -0
  35. data/lib/x/resources/lookups.rb +25 -0
  36. data/lib/x/resources/marshalling.rb +93 -0
  37. data/lib/x/resources/matching_rule.rb +107 -0
  38. data/lib/x/resources/media.rb +278 -0
  39. data/lib/x/resources/media_ids.rb +74 -0
  40. data/lib/x/resources/memo.rb +54 -0
  41. data/lib/x/resources/page.rb +394 -0
  42. data/lib/x/resources/page_limit.rb +80 -0
  43. data/lib/x/resources/pages.rb +270 -0
  44. data/lib/x/resources/parallel.rb +82 -0
  45. data/lib/x/resources/personalized_trend.rb +124 -0
  46. data/lib/x/resources/place.rb +107 -0
  47. data/lib/x/resources/poll.rb +75 -0
  48. data/lib/x/resources/post.rb +615 -0
  49. data/lib/x/resources/post_collections.rb +67 -0
  50. data/lib/x/resources/post_counts.rb +215 -0
  51. data/lib/x/resources/post_search.rb +86 -0
  52. data/lib/x/resources/post_usage.rb +203 -0
  53. data/lib/x/resources/post_writes.rb +140 -0
  54. data/lib/x/resources/published_count.rb +31 -0
  55. data/lib/x/resources/references.rb +121 -0
  56. data/lib/x/resources/relation_writes.rb +54 -0
  57. data/lib/x/resources/relationships.rb +77 -0
  58. data/lib/x/resources/resource.rb +535 -0
  59. data/lib/x/resources/serialization.rb +58 -0
  60. data/lib/x/resources/shape.rb +167 -0
  61. data/lib/x/resources/space.rb +332 -0
  62. data/lib/x/resources/topic.rb +59 -0
  63. data/lib/x/resources/trend.rb +130 -0
  64. data/lib/x/resources/user.rb +502 -0
  65. data/lib/x/resources/user_collections.rb +213 -0
  66. data/lib/x/resources/user_finders.rb +282 -0
  67. data/lib/x/resources/utils.rb +358 -0
  68. data/lib/x/resources/value_equality.rb +38 -0
  69. data/lib/x/resources/value_marshalling.rb +89 -0
  70. data/lib/x/resources/version.rb +25 -0
  71. data/lib/x/resources.rb +22 -0
  72. data/sig/manifest.yaml +7 -0
  73. data/sig/x-resources.rbs +813 -0
  74. metadata +140 -0
@@ -0,0 +1,358 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "uri"
5
+ require_relative "errors"
6
+
7
+ module X
8
+ module Resources
9
+ # Helpers shared across the object layer
10
+ # @api private
11
+ module Utils
12
+ # Parsing options that make a client return plain hashes and arrays, whatever its defaults
13
+ JSON_CLASSES = {array_class: Array, object_class: Hash}.freeze
14
+
15
+ # The pattern of an identifier that is a number, which most resources are identified by
16
+ NUMERIC_ID = /\A\d+\z/
17
+
18
+ # The pattern of the identifiers of each type, as the id_type of a resource class names it: a number; the word
19
+ # characters the identifier of a space or a place is written with; and a media key, which is the number of the
20
+ # type of media and the numeric identifier of the media, joined with an underscore
21
+ ID_PATTERNS = {integer: NUMERIC_ID, raw: /\A\w+\z/, media_key: /\A\d+_\d+\z/}.freeze
22
+
23
+ # What an identifier of each type is, which the error raised for one that is not names
24
+ ID_DESCRIPTIONS = {integer: "an Integer, or a String of digits", raw: "or a String of word characters", media_key: "what an upload returned, or a media key, such as \"3_1880028106020515840\""}.freeze
25
+
26
+ # The pattern of a username: one to fifteen word characters, which the at sign a handle is often written with
27
+ # may precede
28
+ USERNAME = /\A@?\w{1,15}\z/
29
+ private_constant :ID_PATTERNS, :ID_DESCRIPTIONS, :USERNAME
30
+
31
+ # The message of the error raised for a resource of another class than the one an identifier was expected of
32
+ FOREIGN_RESOURCE = "%<given>s %<id>s is not %<expected>s: pass %<expected>s or its identifier"
33
+ # The key a client keeps the identifier of the authenticated user under, named for this gem
34
+ CURRENT_USER_ID = :x_resources_current_user_id
35
+ private_constant :FOREIGN_RESOURCE, :CURRENT_USER_ID
36
+
37
+ extend self
38
+
39
+ # Return a deep-frozen copy of a value with string keys
40
+ #
41
+ # @api private
42
+ # @param value [Object] the value to copy and freeze
43
+ # @return [Object] the frozen copy
44
+ def deep_freeze(value)
45
+ case value
46
+ when Hash then value.transform_keys(&:to_s).transform_values { |element| deep_freeze(element) }.freeze
47
+ when Array then value.map { |element| deep_freeze(element) }.freeze
48
+ when String then value.dup.freeze
49
+ else value
50
+ end
51
+ end
52
+
53
+ # The number of resources a count asks for, converted as Array#first converts it
54
+ #
55
+ # @api private
56
+ # @param count [Object] the count
57
+ # @return [Integer] the count, as an Integer
58
+ # @raise [TypeError] if the count does not convert to an Integer
59
+ # @example Read a count given as a Float
60
+ # X::Resources::Utils.count!(2.5) # => 2
61
+ def count!(count) = Integer.try_convert(count) || raise(TypeError, "no implicit conversion of #{count.class} into Integer")
62
+
63
+ # The attributes a public constructor is given, which must be a Hash
64
+ #
65
+ # Attributes that are not one, such as nil, would otherwise be taken, and raise NoMethodError from a reader, far
66
+ # from where they were given.
67
+ #
68
+ # @api private
69
+ # @param attrs [Hash] the attributes
70
+ # @return [Hash] the attributes
71
+ # @raise [ArgumentError] if the attributes are not a Hash
72
+ # @example Refuse attributes that are not a Hash
73
+ # X::Resources::Utils.attributes!(nil) # raises ArgumentError
74
+ def attributes!(attrs) = Hash.try_convert(attrs) || raise(ArgumentError, "attrs must be a Hash, not #{attrs.inspect}")
75
+
76
+ # Build an endpoint path with an encoded query string
77
+ #
78
+ # @api private
79
+ # @param base [String] the endpoint path without a query string
80
+ # @param params [Hash] the query parameters
81
+ # @return [String] the endpoint path with a query string
82
+ def path(base, params)
83
+ query = query(params)
84
+ return base if query.empty?
85
+
86
+ "#{base}?#{URI.encode_www_form(query)}"
87
+ end
88
+
89
+ # Normalize query parameters into string keys and comma-separated values
90
+ #
91
+ # @api private
92
+ # @param params [Hash] the query parameters
93
+ # @return [Hash{String => String, Integer}] the normalized parameters
94
+ def query(params)
95
+ params.transform_keys(&:to_s).compact.transform_values { |value| query_value(value) }
96
+ end
97
+
98
+ # Normalize the fields of a request body into Symbol keys
99
+ #
100
+ # A caller may name a field by a String or by a Symbol, so each is read by its Symbol, by which a field the
101
+ # object layer sets, such as attachments or reply, is checked and merged, and a body never names a field twice.
102
+ #
103
+ # @api private
104
+ # @param fields [Hash, nil] the fields
105
+ # @return [Hash{Symbol => Object}] the fields, keyed by Symbol
106
+ def fields(fields) = fields.to_h.transform_keys(&:to_sym)
107
+
108
+ # Normalize a query parameter value
109
+ #
110
+ # An Array is joined with commas, and a Time is given in UTC in the ISO 8601 form the API takes.
111
+ #
112
+ # @api private
113
+ # @param value [Object] the value
114
+ # @return [Object] the normalized value
115
+ def query_value(value)
116
+ case value
117
+ when Array then value.join(",")
118
+ when Time then value.getutc.iso8601
119
+ else value
120
+ end
121
+ end
122
+
123
+ # Merge query parameters over defaults, dropping parameters set to nil
124
+ #
125
+ # @api private
126
+ # @param defaults [Hash] the default query parameters
127
+ # @param params [Hash] the query parameters to merge over the defaults
128
+ # @return [Hash{String => String, Integer}] the normalized parameters
129
+ def merge_params(defaults, params)
130
+ query(defaults.merge(params))
131
+ end
132
+
133
+ # Extract the identifier of a resource of a class from the resource or a raw value
134
+ #
135
+ # The identifiers of most resources are numbers, so a value that is not, such as a username, raises rather than
136
+ # reach the API as an identifier it cannot be. The identifiers that are not numbers, such as those of spaces, are
137
+ # word characters, so anything else raises rather than reach the API as part of a path, and a media key is two
138
+ # numbers joined with an underscore, so the numeric identifier of media raises rather than look up nothing. A
139
+ # resource of another class raises too, since its identifier is one of another kind of resource, which the API
140
+ # would read as the identifier of a resource of this class, such as a list followed as though it were a user.
141
+ #
142
+ # @api private
143
+ # @param value [Resource, String, Integer] a resource or an identifier
144
+ # @param klass [Class] the resource class the identifier is of
145
+ # @return [String] the identifier
146
+ # @raise [ArgumentError] if the value is a resource of another class, is neither a resource nor an identifier,
147
+ # or the identifier is not a number, or is not word characters for a resource whose identifiers are not
148
+ # numbers, such as a space, or is not a media key for media
149
+ def id_of(value, klass)
150
+ id = id_from(value, klass)
151
+ return id if id.match?(ID_PATTERNS.fetch(klass.__send__(:id_type)))
152
+
153
+ not_an_identifier(value, klass)
154
+ end
155
+
156
+ # The identifier a value carries, read from a resource or taken as it is
157
+ #
158
+ # The resource this builds checks the identifier when it is made, so this only reads one, and refuses a
159
+ # resource of another class, whose identifier is not one of this class. Anything else that answers id, such as a
160
+ # record of an application's database, is refused too, rather than send an identifier of its own to the API as
161
+ # one of X.
162
+ #
163
+ # @api private
164
+ # @param value [Resource, String, Integer] a resource or an identifier
165
+ # @param klass [Class] the resource class the identifier is of
166
+ # @return [String] the identifier
167
+ # @raise [ArgumentError] if the value is a resource of another class, or is neither a resource nor an identifier
168
+ def id_from(value, klass)
169
+ return value.id.to_s if value.is_a?(klass)
170
+ return value.to_s if value.is_a?(String) || value.instance_of?(Integer)
171
+ raise ArgumentError, format(FOREIGN_RESOURCE, given: value.class, id: value.id, expected: klass) if value.is_a?(Resource)
172
+
173
+ not_an_identifier(value, klass)
174
+ end
175
+
176
+ # Refuse a value that is not an identifier of a resource of a class
177
+ #
178
+ # @api private
179
+ # @param value [Object] the value
180
+ # @param klass [Class] the resource class the identifier is of
181
+ # @return [void]
182
+ # @raise [ArgumentError] always
183
+ def not_an_identifier(value, klass)
184
+ raise ArgumentError, "#{value.inspect} is not an identifier: pass #{klass}, #{ID_DESCRIPTIONS.fetch(klass.__send__(:id_type))}"
185
+ end
186
+
187
+ # Normalize a username, dropping the at sign a handle is often written with
188
+ #
189
+ # @api private
190
+ # @param value [String] the username, with or without a leading at sign
191
+ # @return [String] the username
192
+ def username(value) = value.to_s.delete_prefix("@")
193
+
194
+ # Normalize a username, which must be one, so nothing else reaches a path
195
+ #
196
+ # @api private
197
+ # @param value [String] the username, with or without a leading at sign
198
+ # @return [String] the username, without the at sign
199
+ # @raise [ArgumentError] if the value is not one to fifteen word characters
200
+ def username!(value)
201
+ name = value.to_s
202
+ return name.delete_prefix("@") if name.match?(USERNAME)
203
+
204
+ raise ArgumentError, "#{value.inspect} is not a username: pass one to fifteen letters, digits, or underscores"
205
+ end
206
+
207
+ # The identifier of the user a client's credentials name, when they name one
208
+ #
209
+ # The client's authenticator is where a client keeps the credentials it signs with, and it answers the user
210
+ # they act for; only an OAuth 1.0a access token names one, since it begins with the identifier of the user who
211
+ # authorized it. The secrets a client signs with are its authenticator's to keep, so this asks for the user
212
+ # rather than for a credential to read it out of. An authenticator of another's making may answer the identifier
213
+ # as the String of digits it read off a token, so it is read as an Integer, as every identifier is, and compared
214
+ # with the identifiers of users as one.
215
+ #
216
+ # @api private
217
+ # @param client [Object] the client, whose credentials may name a user
218
+ # @return [Integer, nil] the identifier, or nil if the client's credentials name no user
219
+ # @raise [ArgumentError] if the authenticator answers a user_id that is not an identifier
220
+ def authenticated_user_id(client)
221
+ authenticator = authenticator_of(client)
222
+ Shape.integer(authenticator.user_id) if authenticator.respond_to?(:user_id)
223
+ end
224
+
225
+ # The identifier of the user a lookup found for a client's credentials
226
+ #
227
+ # X::Client keeps it with memoize, under a lock, for the authenticator it holds, and a frozen client keeps it
228
+ # too. Any class can include X::Resources::API, so a client that does not memoize keeps it in an instance variable
229
+ # named for this gem, rather than one named @current_user_id, as an application often names its own, which
230
+ # would be overwritten.
231
+ #
232
+ # @api private
233
+ # @param client [Object] the client
234
+ # @return [Integer, nil] the identifier, or nil if none was found for the authenticator the client holds
235
+ def remembered_user_id(client)
236
+ return client.memoized(CURRENT_USER_ID) if client.respond_to?(:memoized)
237
+
238
+ owner, id = client.instance_variable_get(:@x_resources_current_user_id)
239
+ id if owner.equal?(authenticator_of(client))
240
+ end
241
+
242
+ # Keep the identifier of the authenticated user, with the authenticator
243
+ #
244
+ # A frozen client that does not memoize keeps nothing.
245
+ #
246
+ # @api private
247
+ # @param client [Object] the client
248
+ # @param id [Integer] the identifier
249
+ # @return [void]
250
+ def remember_user_id(client, id)
251
+ if client.respond_to?(:memoize)
252
+ client.memoize(CURRENT_USER_ID, id)
253
+ elsif !client.frozen?
254
+ client.instance_variable_set(:@x_resources_current_user_id, [authenticator_of(client), id])
255
+ end
256
+ end
257
+
258
+ # The authenticator of a client, which is replaced whenever its credentials change
259
+ #
260
+ # @api private
261
+ # @param client [Object] the client, which may have an authenticator
262
+ # @return [Object, nil] the authenticator, or nil if the client has none
263
+ def authenticator_of(client)
264
+ client.authenticator if client.respond_to?(:authenticator)
265
+ end
266
+
267
+ # The client for an endpoint that refuses OAuth 1.0a
268
+ #
269
+ # A space endpoint refuses it, and so does one that takes app-only authentication alone, as the count of the
270
+ # full archive does. A client that signs with OAuth 1.0a requests as the app, with a copy that reuses its bearer token, as one
271
+ # signed in with OAuth 2.0 as a user that holds the app's credentials does. One that holds none, and so has no
272
+ # app-only client, requests as the user, and the API answers as it answers those credentials: an endpoint that
273
+ # takes OAuth 2.0 user authentication answers it, and one that takes app-only authentication alone refuses it
274
+ # with 403 Forbidden, which raises X::Forbidden, as any request refused for its credentials does.
275
+ #
276
+ # @api private
277
+ # @param client [Object] the client
278
+ # @return [Object] the client's app-only client, which reuses its bearer token, or the client itself
279
+ def app_client(client)
280
+ client.respond_to?(:app_only) ? client.app_only : client
281
+ rescue UnsupportedOperation
282
+ client
283
+ end
284
+
285
+ # Check whether a value identifies a resource rather than naming one
286
+ #
287
+ # An Integer is an identifier and a String is a name, such as a username, so that an
288
+ # account whose username is all digits is looked up as the name it is.
289
+ #
290
+ # @api private
291
+ # @param value [Resource, String, Integer] a resource, an identifier, or a username
292
+ # @return [Boolean] true if the value is a resource or an Integer identifier
293
+ def id?(value)
294
+ value.is_a?(Resource) || value.instance_of?(Integer)
295
+ end
296
+
297
+ # Read a value of the data of the response to a write
298
+ #
299
+ # It is such a value as whether a user is now followed.
300
+ #
301
+ # @api private
302
+ # @param body [Hash, nil] the parsed body of the response
303
+ # @param key [String] the key of the value in the data
304
+ # @return [Object, nil] the value, or nil if the response holds no data or no such value
305
+ # @raise [InvalidAttribute] if the data of the response is not an object
306
+ # @example Read whether a post was deleted
307
+ # X::Resources::Utils.written(body, "deleted")
308
+ def written(body, key) = Shape.read_object("the data of the response", body.to_h["data"])&.[](key)
309
+
310
+ # Read a value of a response, which must be what the API documents it to be
311
+ #
312
+ # @api private
313
+ # @param name [String] what the value is, such as the reader that reads it
314
+ # @param value [Object] the value the response holds
315
+ # @yieldparam value [Object] the value
316
+ # @return [Object] what the block returns
317
+ # @raise [InvalidAttribute] if the block raises ArgumentError for the value
318
+ # @example Read the time a post was created
319
+ # X::Resources::Utils.read("X::Post#created_at", attrs["created_at"]) { |value| X::Resources::Utils.time(value) }
320
+ def read(name, value)
321
+ yield value
322
+ rescue ArgumentError
323
+ raise InvalidAttribute, "#{name} cannot be read from #{value.inspect}"
324
+ end
325
+
326
+ # Deconstruct a value into what its readers read, so it matches a hash pattern
327
+ #
328
+ # A pattern that asks for every key gets each reader by the name it is declared by, and one that names keys
329
+ # gets those of them it names, by any name they are read by, as a resource matches a pattern.
330
+ #
331
+ # @api private
332
+ # @param value [Object] the value, which answers each reader
333
+ # @param keys [Array<Symbol>, nil] the keys the pattern asks for, or nil for every reader
334
+ # @param names [Array<Symbol>] the readers
335
+ # @param aliases [Array<Symbol>] the other names some of the readers are read by
336
+ # @return [Hash{Symbol => Object}] what the readers the pattern asks for read
337
+ # @example Deconstruct a trend
338
+ # X::Resources::Utils.deconstruct(trend, [:name], %i[name post_count], %i[tweet_count]) # => {name: "#ruby"}
339
+ def deconstruct(value, keys, names, aliases = [])
340
+ names = (names + aliases) & keys unless keys.nil?
341
+ names.to_h { |name| [name, value.public_send(name)] }
342
+ end
343
+
344
+ # Parse an ISO 8601 timestamp
345
+ #
346
+ # A value that is not a String, such as a number, is not ISO 8601 either, and raises ArgumentError as one.
347
+ #
348
+ # @api private
349
+ # @param value [String, nil] the timestamp
350
+ # @return [Time, nil] the parsed time or nil if the timestamp is missing
351
+ # @raise [ArgumentError] if the timestamp is not ISO 8601
352
+ def time(value)
353
+ Time.iso8601(value.to_s) unless value.nil?
354
+ end
355
+ end
356
+ private_constant :Utils
357
+ end
358
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X
4
+ module Resources
5
+ # Equality of what an API response held that has no identifier, by its class and attributes
6
+ #
7
+ # A trend or the usage of a project has no identifier to tell it by, and a rule a post matched is told by its tag
8
+ # as well as its identifier, so two are equal when they are of the same class and hold the same attributes, and
9
+ # equal ones share a hash, so that uniq and a Hash key tell them apart. Every attribute counts, so a topic that
10
+ # trends in two places, with a count of posts in each, is two trends that differ; compare their names to find the
11
+ # topics two places share, as trends.map(&:name).intersect?(other_trends.map(&:name)).
12
+ #
13
+ # Internal to x-resources: the methods it gives X::Trend, X::PersonalizedTrend, X::PostUsage, and X::MatchingRule are
14
+ # public API, but the module is only how they are shared, and which classes include it can change within 1.x.
15
+ #
16
+ # @api semipublic
17
+ module ValueEquality
18
+ # Check whether another object holds the same attributes
19
+ #
20
+ # @api public
21
+ # @param other [Object] the other object
22
+ # @return [Boolean] true if the other is of the same class and holds the same attributes
23
+ # @example Check whether the trends of the world changed since they were last read, a count included
24
+ # X::Trend.at(1, client: client) == trends
25
+ def ==(other) = other.instance_of?(self.class) && attrs.eql?((_ = other).attrs)
26
+ alias_method :eql?, :==
27
+
28
+ # The hash of the object, which equal objects share
29
+ #
30
+ # @api public
31
+ # @return [Integer] the hash
32
+ # @example Collect the distinct rules the posts of a stream matched
33
+ # posts.flat_map(&:matching_rules).uniq
34
+ def hash = [self.class, attrs].hash
35
+ end
36
+ private_constant :ValueEquality
37
+ end
38
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module X
6
+ module Resources
7
+ # The state Marshal writes and reads of what an API response held that is not a resource
8
+ #
9
+ # A trend, the usage of a project, and a rule a post matched hold their attributes alone, so what is written is
10
+ # those attributes, led by the number of their format, as a resource is written, and what is read is built of
11
+ # them as the constructor builds it, frozen.
12
+ #
13
+ # Internal to x-resources: the methods it gives X::Trend, X::PersonalizedTrend, X::PostUsage, and X::MatchingRule,
14
+ # marshal_dump, marshal_load, encode_with, and init_with, are public API, but the module is only how they are shared,
15
+ # and which classes include it can change within 1.x.
16
+ #
17
+ # @api semipublic
18
+ module ValueMarshalling
19
+ # The number of the format of the state Marshal writes, which every release of 1.x writes
20
+ #
21
+ # A later release of 1.x adds to the state only what an earlier one ignores, parts after those it reads and keys of a
22
+ # Hash it does not read, so that the state one release of 1.x writes is read by every other, earlier or later.
23
+ #
24
+ # @api private
25
+ MARSHAL_FORMAT = 1
26
+ # The name YAML writes each part of the state under, in the order Marshal writes them
27
+ # @api private
28
+ YAML_KEYS = %w[format attrs].freeze
29
+ private_constant :MARSHAL_FORMAT, :YAML_KEYS
30
+
31
+ # The state Marshal writes
32
+ #
33
+ # What is written is plain data, led by the number of its format, so that a value written by one release of 1.x
34
+ # is read by a later one: its attributes, as the API sent them.
35
+ #
36
+ # @api public
37
+ # @return [Array(Integer, Hash{String => Object})] the number of the format, then the attributes
38
+ # @example Cache the trends of a place
39
+ # Rails.cache.write("trends", X::Trend.at(1, client: client))
40
+ def marshal_dump = [MARSHAL_FORMAT, attrs]
41
+
42
+ # Restore a value Marshal read, frozen as the value that was written was
43
+ #
44
+ # @api public
45
+ # @param state [Array] the state Marshal wrote
46
+ # @return [void]
47
+ # @raise [UnsupportedFormat] if the state is of a format this release does not read
48
+ # @example Read cached trends
49
+ # Marshal.load(Marshal.dump(trend)).name
50
+ def marshal_load(state)
51
+ format, attrs = state
52
+ raise UnsupportedFormat, "#{self.class} reads format #{MARSHAL_FORMAT} of Marshal, not #{format.inspect}" unless MARSHAL_FORMAT.eql?(format)
53
+
54
+ restore(attrs)
55
+ end
56
+
57
+ # Write the state Marshal writes as YAML
58
+ #
59
+ # YAML would write the instance variables of the value, and read them back into one that is not frozen, so it says
60
+ # how it is written: each part of the state Marshal writes, under its name.
61
+ #
62
+ # @api public
63
+ # @param coder [Psych::Coder] the coder YAML writes the value with
64
+ # @return [void]
65
+ # @example Write a value as YAML
66
+ # YAML.dump(trend)
67
+ def encode_with(coder) = YAML_KEYS.zip(marshal_dump) { |key, value| coder[key] = value }
68
+
69
+ # Restore a value YAML read, frozen, as Marshal restores one
70
+ #
71
+ # @api public
72
+ # @param coder [Psych::Coder] the coder YAML read the value with
73
+ # @return [void]
74
+ # @raise [UnsupportedFormat] if the state is of a format this release does not read
75
+ # @example Read a value written as YAML
76
+ # YAML.unsafe_load(YAML.dump(trend)).name
77
+ def init_with(coder) = marshal_load(coder.map.values_at(*YAML_KEYS))
78
+
79
+ private
80
+
81
+ # Build the value of the attributes Marshal read, as its constructor builds it
82
+ # @api private
83
+ # @param attrs [Hash{String => Object}] the attributes
84
+ # @return [void]
85
+ def restore(attrs) = initialize(attrs) # steep:ignore UnexpectedPositionalArgument
86
+ end
87
+ private_constant :ValueMarshalling
88
+ end
89
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems/version"
4
+
5
+ module X
6
+ # The object layer of the X gem
7
+ # @api public
8
+ module Resources
9
+ # The current version of the x-resources gem
10
+ # @api public
11
+ VERSION = "1.0.0"
12
+
13
+ # The version as a Gem::Version, which compares one release with another
14
+ #
15
+ # VERSION is a String, as a version constant is throughout Ruby, so that what reads it can split it, match it,
16
+ # or send it wherever a String belongs. This builds the Gem::Version that compares it with another version,
17
+ # which a String compares by character rather than by segment.
18
+ #
19
+ # @api public
20
+ # @return [Gem::Version] the version
21
+ # @example Take a path that a later release opened
22
+ # X::Resources.gem_version >= Gem::Version.new("1.1")
23
+ def self.gem_version = Gem::Version.new(VERSION)
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "x/core"
4
+ require_relative "resources/version"
5
+ require_relative "resources/errors"
6
+ require_relative "resources/api"
7
+ require_relative "resources/relationships"
8
+ require_relative "resources/bookmark_folder"
9
+ require_relative "resources/community"
10
+ require_relative "resources/cursor"
11
+ require_relative "resources/direct_message"
12
+ require_relative "resources/list"
13
+ require_relative "resources/media"
14
+ require_relative "resources/place"
15
+ require_relative "resources/poll"
16
+ require_relative "resources/post"
17
+ require_relative "resources/space"
18
+ require_relative "resources/topic"
19
+ require_relative "resources/trend"
20
+ require_relative "resources/personalized_trend"
21
+ require_relative "resources/user"
22
+ require_relative "resources/post_usage"
data/sig/manifest.yaml ADDED
@@ -0,0 +1,7 @@
1
+ # The standard libraries the signatures of x-resources refer to, which rbs collection loads for code that depends on it
2
+ #
3
+ # rbs collection reads every library named here as a standard library, so the gems x-resources depends on are left to its
4
+ # gemspec, from which it installs their signatures.
5
+ dependencies:
6
+ - name: json
7
+ - name: uri