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,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "utils"
4
+
5
+ module X
6
+ module Resources
7
+ # Reads the objects and lists of a response, which must be what the API documents them to be
8
+ #
9
+ # A value the object layer reads into, such as the public_metrics a count is read from, or the links of a post, is
10
+ # checked to be an object or a list before it is read, so that one that is not raises InvalidAttribute, as any
11
+ # value of a response that cannot be read does, rather than the NoMethodError or TypeError of reading into it.
12
+ #
13
+ # @api private
14
+ module Shape
15
+ # The pattern of a conversation identifier: two user identifiers joined with a hyphen, or a group's own number
16
+ CONVERSATION_ID = /\A\d+(?:-\d+)?\z/
17
+
18
+ extend self
19
+
20
+ # Read the value at a path of keys, through the object each key but the last names
21
+ #
22
+ # A key that names nothing reads as nil, as Hash#dig does, but one that names something other than an object,
23
+ # such as a String where the API documents public_metrics, cannot be read further.
24
+ #
25
+ # @api private
26
+ # @param name [String] what the value is, such as the reader that reads it
27
+ # @param value [Hash, nil] the object the path starts at
28
+ # @param path [Array<String>] the keys
29
+ # @return [Object, nil] the value, or nil if a key names nothing
30
+ # @raise [InvalidAttribute] if the path passes through something other than an object
31
+ # @example Read the number of times a post was liked
32
+ # X::Resources::Shape.dig("X::Post#like_count", attrs, %w[public_metrics like_count])
33
+ def dig(name, value, path) = path.reduce(value) { |current, key| read_object(name, current)&.[](key) }
34
+
35
+ # Read the value at the first of several paths of keys that names one
36
+ #
37
+ # @api private
38
+ # @param name [String] what the value is, such as the reader that reads it
39
+ # @param value [Hash, nil] the object the paths start at
40
+ # @param paths [Array<Array<String>>] the paths, in the order they are tried
41
+ # @return [Object, nil] the value, or nil if no path names one
42
+ # @raise [InvalidAttribute] if a path passes through something other than an object
43
+ # @example Read the number of times a post was reposted, by either name
44
+ # X::Resources::Shape.dig_first("X::Post#repost_count", attrs, [%w[public_metrics repost_count], %w[public_metrics retweet_count]])
45
+ def dig_first(name, value, paths)
46
+ paths.each do |path|
47
+ found = dig(name, value, path)
48
+ return found unless found.nil?
49
+ end
50
+ nil
51
+ end
52
+
53
+ # Read a String, which the response may leave out
54
+ #
55
+ # @api private
56
+ # @param name [String] what the value is, such as the reader that reads it
57
+ # @param value [String, nil] the String
58
+ # @return [String, nil] the String, or nil if the value is missing
59
+ # @raise [InvalidAttribute] if the value is not a String
60
+ # @example Read the username a permalink names
61
+ # X::Resources::Shape.read_string("X::User#permalink", user.username)
62
+ def read_string(name, value) = Utils.read(name, value) { String.try_convert(value) || (raise ArgumentError unless value.nil?) }
63
+
64
+ # Read an object, which the response may leave out
65
+ #
66
+ # @api private
67
+ # @param name [String] what the value is, such as the reader that reads it
68
+ # @param value [Hash, nil] the object
69
+ # @return [Hash, nil] the object, or nil if the value is missing
70
+ # @raise [InvalidAttribute] if the value is not an object
71
+ # @example Read the entities of a post
72
+ # X::Resources::Shape.read_object("X::Post#entities", attrs["entities"])
73
+ def read_object(name, value) = Utils.read(name, value) { object(value) }
74
+
75
+ # Read a list of objects, which the response may leave out
76
+ #
77
+ # @api private
78
+ # @param name [String] what the value is, such as the reader that reads it
79
+ # @param value [Array<Hash>, nil] the list
80
+ # @return [Array<Hash>] the objects, empty if the list is missing
81
+ # @raise [InvalidAttribute] if the value is not a list, or holds something other than an object
82
+ # @example Read the links of a post
83
+ # X::Resources::Shape.objects("X::Post#urls", entities["urls"])
84
+ def objects(name, value) = Utils.read(name, value) { object_list(value) }
85
+
86
+ # Check that a value the API documents as a list of objects is one, if it is there
87
+ #
88
+ # @api private
89
+ # @param value [Array<Hash>, nil] the value
90
+ # @return [Array<Hash>] the objects, empty if the value is missing
91
+ # @raise [ArgumentError] if the value is not a list, or holds something other than an object
92
+ def object_list(value) = Array(list(value)).each { |element| object!(element) }.freeze
93
+
94
+ # Check that a value the API documents as an object is one, if it is there
95
+ #
96
+ # @api private
97
+ # @param value [Hash, nil] the value
98
+ # @return [Hash, nil] the object, or nil if the value is missing
99
+ # @raise [ArgumentError] if the value is not an object
100
+ def object(value) = (object!(value) unless value.nil?)
101
+
102
+ # Check that a value the API documents as an object is one
103
+ #
104
+ # @api private
105
+ # @param value [Hash] the value
106
+ # @return [Hash] the object
107
+ # @raise [ArgumentError] if the value is not an object
108
+ def object!(value) = Hash.try_convert(value) || raise(ArgumentError, "#{value.inspect} is not an object")
109
+
110
+ # Check that a value the API documents as a list is one, if it is there
111
+ #
112
+ # @api private
113
+ # @param value [Array, nil] the value
114
+ # @return [Array, nil] the list, or nil if the value is missing
115
+ # @raise [ArgumentError] if the value is not a list
116
+ def list(value) = (Array.try_convert(value) || raise(ArgumentError, "#{value.inspect} is not a list") unless value.nil?)
117
+
118
+ # Read a range of characters, which the response may leave out
119
+ #
120
+ # The API gives a range as a list of its start and its end, which the range leaves out.
121
+ #
122
+ # @api private
123
+ # @param value [Array<Integer>, nil] the start and end of the range
124
+ # @return [Range<Integer>, nil] the range, which leaves out its end, or nil if the value is missing
125
+ # @raise [ArgumentError] if the value is not a list of two Integers that are not negative
126
+ # @example Read the range of the text a post shows
127
+ # X::Resources::Shape.range([8, 10]) # => 8...10
128
+ def range(value)
129
+ bounds = list(value)
130
+ return if bounds.nil?
131
+ raise ArgumentError, "#{bounds} is not the start and end of a range" unless bounds.size.eql?(2) && bounds.all? { |bound| Integer === bound && !bound.negative? }
132
+
133
+ Range.new(bounds.first, bounds.last, true)
134
+ end
135
+
136
+ # Read a numeric identifier or a count as an Integer, if it is there
137
+ #
138
+ # It is read as strictly as an identifier is checked: an Integer that is not negative, or a String of digits
139
+ # alone, with no sign, underscore, or whitespace, so that a count reads as the whole number it is, and the
140
+ # identifier of a resource a reader such as author_id returns is one that resource is found by.
141
+ #
142
+ # @api private
143
+ # @param value [String, Integer, nil] the identifier or count
144
+ # @return [Integer, nil] the Integer, or nil if the value is missing
145
+ # @raise [ArgumentError] if the value is neither an Integer that is not negative nor a String of digits
146
+ def integer(value)
147
+ return value if value.nil? || (Integer === value && !value.negative?)
148
+ raise ArgumentError, "invalid value for Integer(): #{value.to_s.inspect}" unless String === value && Utils::NUMERIC_ID.match?(value)
149
+
150
+ Integer(value, 10)
151
+ end
152
+
153
+ # Read the identifier of a conversation of direct messages, if it is there
154
+ #
155
+ # @api private
156
+ # @param value [String, nil] the identifier
157
+ # @return [String, nil] the identifier, or nil if the value is missing
158
+ # @raise [ArgumentError] if the value is not a String of digits, or of two numbers joined with a hyphen
159
+ def conversation_id(value)
160
+ return value if value.nil? || (String === value && CONVERSATION_ID.match?(value))
161
+
162
+ raise ArgumentError, "#{value.inspect} is not a conversation identifier"
163
+ end
164
+ end
165
+ private_constant :Shape
166
+ end
167
+ end
@@ -0,0 +1,332 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "batch_finders"
4
+ require_relative "cursor"
5
+ require_relative "resource"
6
+ require_relative "topic"
7
+
8
+ module X
9
+ module Resources
10
+ # A live audio space
11
+ #
12
+ # The space endpoints, but for buyers, take app-only authentication or OAuth 2.0 user context, not OAuth 1.0a, so
13
+ # a client that signs with OAuth 1.0a reads spaces with a copy that authenticates as the app, while the spaces and
14
+ # posts it reads hold the client, so that they act as the user.
15
+ #
16
+ # @api public
17
+ class ::X::Space < Resource
18
+ extend BatchFinders
19
+
20
+ # The mixins of the class by their full names, which YARD needs to resolve them in a class opened under X
21
+ #
22
+ # @!parse
23
+ # extend X::Resources::BatchFinders
24
+
25
+ # Every public space field
26
+ #
27
+ # A minor release may add to it the fields the API adds, so that a lookup asks for them too; see
28
+ # {Resource#hydrated?} for what that means for a resource looked up with a list of fields of its own.
29
+ FIELDS = %w[created_at ended_at id is_ticketed lang participant_count scheduled_start started_at state
30
+ subscriber_count title updated_at].freeze
31
+ # Every expansion available on space endpoints
32
+ #
33
+ # A minor release may add to it the expansions the API adds, so that a lookup asks for them too; see
34
+ # {Resource#hydrated?} for what that means for a resource looked up with a list of expansions of its own.
35
+ EXPANSIONS = %w[creator_id host_ids invited_user_ids speaker_ids topic_ids].freeze
36
+ # Maximum number of posts or buyers per page, or of spaces a search returns
37
+ # @api private
38
+ MAX_RESULTS = 100
39
+ private_constant :MAX_RESULTS
40
+
41
+ class << self
42
+ # The API endpoint used to look up spaces by identifier
43
+ #
44
+ # @api private
45
+ # @return [String] the endpoint
46
+ # @example Get the endpoint
47
+ # X::Space.__send__(:endpoint) # => "spaces"
48
+ def endpoint
49
+ "spaces"
50
+ end
51
+
52
+ # The type of the identifier, which is letters and digits rather than a number
53
+ #
54
+ # @api private
55
+ # @return [Symbol] raw
56
+ # @example Get the identifier type
57
+ # X::Space.__send__(:id_type) # => :raw
58
+ def id_type = :raw
59
+
60
+ # The client a space lookup requests with
61
+ #
62
+ # The space endpoints refuse OAuth 1.0a, so a client that signs with it looks spaces up with a copy that
63
+ # reuses its bearer token, as a client signed in with OAuth 2.0 as a user that holds the app's credentials does.
64
+ # One that holds none looks them up as it is.
65
+ #
66
+ # @api private
67
+ # @param client [Object] the client the lookup was given
68
+ # @return [Object] the client's app-only client, or the client itself
69
+ # @example Get the client a space lookup requests with
70
+ # X::Space.__send__(:client_for, client)
71
+ def client_for(client) = Utils.app_client(client)
72
+
73
+ # The query parameter that selects space fields
74
+ #
75
+ # @api private
76
+ # @return [String] the fields parameter
77
+ # @example Get the fields parameter
78
+ # X::Space.__send__(:fields_key) # => "space.fields"
79
+ def fields_key = "space.fields"
80
+
81
+ private :endpoint, :id_type, :client_for, :fields_key
82
+
83
+ # The default query parameters requesting every space field and expansion
84
+ #
85
+ # @api public
86
+ # @return [Hash{String => Array<String>}] the default query parameters
87
+ # @example Get the default parameters
88
+ # X::Space.default_params["space.fields"]
89
+ def default_params
90
+ {"space.fields" => FIELDS, "user.fields" => User::FIELDS, "topic.fields" => Topic::FIELDS, "expansions" => EXPANSIONS}
91
+ end
92
+
93
+ # Search spaces by their titles
94
+ #
95
+ # The API returns the matching spaces in one response, of up to 100 spaces.
96
+ #
97
+ # @api public
98
+ # @param query [String] the search query
99
+ # @param client [Object] the client used to make the request
100
+ # @param params [Hash] query parameters merged over the default parameters, such as state: live or scheduled
101
+ # @return [Cursor] a cursor over the matching spaces
102
+ # @example Print the live spaces about Ruby
103
+ # X::Space.search("ruby", client: client, state: "live").each { |space| puts space.title }
104
+ def search(query, client:, **params)
105
+ Cursor.__send__(:build, self, "spaces/search", client:, params: {query:, max_results: MAX_RESULTS}.merge(params), app_only: true)
106
+ end
107
+ end
108
+
109
+ # Look up the live and scheduled spaces many users created, in parallel batches
110
+ #
111
+ # The API returns the spaces of up to 100 users at a time, in one response without pages, so the users are
112
+ # looked up that many at a time.
113
+ #
114
+ # @api public
115
+ # @param users [Array<User, String, Integer>] the users who created the spaces, or their identifiers
116
+ # @param client [Object] the client used to make the requests
117
+ # @param concurrency [Integer] the number of batches looked up at once, which must be at least one
118
+ # @param params [Hash] query parameters merged over the default parameters
119
+ # @return [Array<Space>] the spaces, frozen, empty if the users created none
120
+ # @raise [ArgumentError] if a user is not a user or the identifier of one, or the concurrency is less than one,
121
+ # before a request
122
+ # @yieldparam problem [Problem] each problem the API reported
123
+ # @example Print the spaces two users created
124
+ # X::Space.find_all_by_creator([7505382, 783214], client: client).each { |space| puts space.title }
125
+ def self.find_all_by_creator(users, client:, concurrency: BatchFinders::DEFAULT_CONCURRENCY, **params, &)
126
+ ids = users.map { |user| Utils.id_of(user, User) }
127
+ spaces = lookup_in_batches("spaces/by/creator_ids", :user_ids, ids, client:, concurrency:, **params, &) #: Array[Space]
128
+ spaces.freeze
129
+ end
130
+
131
+ # @!attribute [r] title
132
+ # The title
133
+ # @api public
134
+ # @return [String, nil] the title
135
+ # @example Get the title
136
+ # space.title
137
+ attribute :title
138
+
139
+ # @!attribute [r] state
140
+ # The state: live, scheduled, or ended
141
+ # @api public
142
+ # @return [String, nil] the state
143
+ # @example Get the state
144
+ # space.state
145
+ attribute :state
146
+
147
+ # @!attribute [r] lang
148
+ # The BCP 47 language tag
149
+ # @api public
150
+ # @return [String, nil] the language tag
151
+ # @example Get the language
152
+ # space.lang
153
+ attribute :lang
154
+
155
+ # @!attribute [r] created_at
156
+ # The time when the space was created
157
+ # @api public
158
+ # @return [Time, nil] the creation time
159
+ # @example Get the creation time
160
+ # space.created_at
161
+ attribute :created_at, :time
162
+
163
+ # @!attribute [r] started_at
164
+ # The time when the space started
165
+ # @api public
166
+ # @return [Time, nil] the start time
167
+ # @example Get the start time
168
+ # space.started_at
169
+ attribute :started_at, :time
170
+
171
+ # @!attribute [r] ended_at
172
+ # The time when the space ended
173
+ # @api public
174
+ # @return [Time, nil] the end time
175
+ # @example Get the end time
176
+ # space.ended_at
177
+ attribute :ended_at, :time
178
+
179
+ # @!attribute [r] scheduled_start
180
+ # The scheduled start time
181
+ # @api public
182
+ # @return [Time, nil] the scheduled start time
183
+ # @example Get the scheduled start time
184
+ # space.scheduled_start
185
+ attribute :scheduled_start, :time
186
+
187
+ # @!attribute [r] updated_at
188
+ # The time when the space was last updated
189
+ # @api public
190
+ # @return [Time, nil] the update time
191
+ # @example Get the update time
192
+ # space.updated_at
193
+ attribute :updated_at, :time
194
+
195
+ # @!attribute [r] ticketed
196
+ # Whether the space requires a ticket, the is_ticketed field
197
+ # @api public
198
+ # @return [Boolean, nil] true if the space is ticketed
199
+ # @example Check whether a space is ticketed
200
+ # space.ticketed?
201
+ attribute :ticketed, :boolean, key: %w[is_ticketed]
202
+
203
+ # @!method ticketed?
204
+ # Check whether the space requires a ticket
205
+ # @api public
206
+ # @return [Boolean] true if the space is ticketed
207
+ # @example Check whether a space is ticketed
208
+ # space.ticketed?
209
+
210
+ # @!attribute [r] participant_count
211
+ # The number of participants
212
+ # @api public
213
+ # @return [Integer, nil] the participant count
214
+ # @example Get the participant count
215
+ # space.participant_count
216
+ attribute :participant_count, :integer
217
+
218
+ # @!attribute [r] subscriber_count
219
+ # The number of subscribers
220
+ # @api public
221
+ # @return [Integer, nil] the subscriber count
222
+ # @example Get the subscriber count
223
+ # space.subscriber_count
224
+ attribute :subscriber_count, :integer
225
+
226
+ # @!attribute [r] creator_id
227
+ # The identifier of the creator
228
+ # @api public
229
+ # @return [Integer, nil] the creator identifier
230
+ # @example Get the creator identifier
231
+ # space.creator_id
232
+ attribute :creator_id, :integer
233
+
234
+ # @!attribute [r] host_ids
235
+ # The identifiers of the hosts
236
+ # @api public
237
+ # @return [Array<Integer>] the host identifiers, empty if there are none
238
+ # @example Get the host identifiers
239
+ # space.host_ids
240
+ attribute :host_ids, :integers
241
+
242
+ # @!attribute [r] speaker_ids
243
+ # The identifiers of the speakers
244
+ # @api public
245
+ # @return [Array<Integer>] the speaker identifiers, empty if there are none
246
+ # @example Get the speaker identifiers
247
+ # space.speaker_ids
248
+ attribute :speaker_ids, :integers
249
+
250
+ # @!attribute [r] invited_user_ids
251
+ # The identifiers of the invited users
252
+ # @api public
253
+ # @return [Array<Integer>] the invited user identifiers, empty if there are none
254
+ # @example Get the invited user identifiers
255
+ # space.invited_user_ids
256
+ attribute :invited_user_ids, :integers
257
+
258
+ # @!attribute [r] topic_ids
259
+ # The identifiers of the topics
260
+ # @api public
261
+ # @return [Array<Integer>] the topic identifiers, empty if there are none
262
+ # @example Get the topic identifiers
263
+ # space.topic_ids
264
+ attribute :topic_ids, :integers
265
+
266
+ # @!method creator
267
+ # The creator, resolved from the includes or as a stub holding only its identifier
268
+ # @api public
269
+ # @return [User, nil] the creator
270
+ # @example Get the creator's username
271
+ # space.creator.username
272
+ reference :creator, :User, key: %w[creator_id]
273
+
274
+ # @!method hosts
275
+ # The hosts, resolved from the includes or as stubs holding only their identifiers
276
+ # @api public
277
+ # @return [Array<User>] the hosts
278
+ # @example Get the hosts
279
+ # space.hosts
280
+ references :hosts, :User, key: %w[host_ids]
281
+
282
+ # @!method speakers
283
+ # The speakers, from the includes or as stubs holding only their identifiers
284
+ # @api public
285
+ # @return [Array<User>] the speakers
286
+ # @example Get the speakers
287
+ # space.speakers
288
+ references :speakers, :User, key: %w[speaker_ids]
289
+
290
+ # @!method invited_users
291
+ # The invited users, from the includes or as stubs holding only their identifiers
292
+ # @api public
293
+ # @return [Array<User>] the invited users
294
+ # @example Get the invited users
295
+ # space.invited_users
296
+ references :invited_users, :User, key: %w[invited_user_ids]
297
+
298
+ # @!method topics
299
+ # The topics, from the includes or as stubs holding only their identifiers
300
+ # @api public
301
+ # @return [Array<Topic>] the topics
302
+ # @example Get the names of the topics
303
+ # space.topics.map(&:name)
304
+ references :topics, :Topic, key: %w[topic_ids]
305
+
306
+ # The posts shared in this space
307
+ #
308
+ # @api public
309
+ # @param params [Hash] query parameters merged over the default parameters
310
+ # @return [Cursor] a cursor over the posts
311
+ # @example Print the shared posts
312
+ # space.posts.each { |post| puts post.text }
313
+ def posts(**params)
314
+ cursor(Post, "spaces/#{id}/tweets", max_results: MAX_RESULTS, app_only: true, **params)
315
+ end
316
+
317
+ # The users who bought a ticket to this space
318
+ #
319
+ # The authenticated user must have created the space. The endpoint takes only OAuth 2.0 user context, which the object layer cannot route around, so a client that
320
+ # signs with OAuth 1.0a, or authenticates as the app, is refused.
321
+ #
322
+ # @api public
323
+ # @param params [Hash] query parameters merged over the default parameters
324
+ # @return [Cursor] a cursor over the buyers
325
+ # @example Print the buyers of a ticketed space
326
+ # space.buyers.each { |user| puts user.username }
327
+ def buyers(**params) = cursor(User, "spaces/#{id}/buyers", max_results: MAX_RESULTS, **params)
328
+
329
+ alias_method :tweets, :posts
330
+ end
331
+ end
332
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "resource"
4
+
5
+ module X
6
+ # A topic a space is about
7
+ #
8
+ # The API offers no lookup of topics, so a topic is read from the response of the space that expanded it, and the
9
+ # class answers no finder. from_id builds one from its identifier, which a topic the response did not expand is built
10
+ # as too, and which compares equal to the topic it identifies, but hydrate and refresh raise UnsupportedOperation for
11
+ # one that is not hydrated, since there is nothing to look it up with.
12
+ #
13
+ # @api public
14
+ class Topic < Resource
15
+ # Every public topic field
16
+ #
17
+ # A minor release may add to it the fields the API adds, so that a lookup asks for them too; see
18
+ # {Resource#hydrated?} for what that means for a resource looked up with a list of fields of its own.
19
+ FIELDS = %w[description id name].freeze
20
+
21
+ # The default query parameters, which request every field
22
+ #
23
+ # The API offers no lookup of topics, so they are requested by the spaces that expand them, which ask for these
24
+ # fields, and a topic a response included with all of them is hydrated.
25
+ #
26
+ # @api public
27
+ # @return [Hash{String => Array<String>}] the default query parameters
28
+ # @example Get the default parameters
29
+ # X::Topic.default_params # => {"topic.fields" => [...]}
30
+ def self.default_params = {"topic.fields" => FIELDS}
31
+
32
+ # The key under which topics appear in the includes of a response
33
+ #
34
+ # @api private
35
+ # @return [String] the includes key
36
+ # @example Get the includes key
37
+ # X::Topic.__send__(:includes_key) # => "topics"
38
+ def self.includes_key
39
+ "topics"
40
+ end
41
+ private_class_method :includes_key
42
+
43
+ # @!attribute [r] name
44
+ # The name of the topic
45
+ # @api public
46
+ # @return [String, nil] the name
47
+ # @example Get the name
48
+ # topic.name # => "Technology"
49
+ attribute :name
50
+
51
+ # @!attribute [r] description
52
+ # What the topic is about
53
+ # @api public
54
+ # @return [String, nil] the description
55
+ # @example Get the description
56
+ # topic.description # => "All about technology"
57
+ attribute :description
58
+ end
59
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "serialization"
4
+ require_relative "shape"
5
+ require_relative "utils"
6
+ require_relative "value_equality"
7
+ require_relative "value_marshalling"
8
+
9
+ module X
10
+ module Resources
11
+ # A topic trending in a place, as the trends of the place report it
12
+ #
13
+ # A trend has no identifier, and the API offers no lookup of one, so trends are read a place at a time, a place
14
+ # named by its Yahoo! Where On Earth identifier (WOEID), such as 1 for the whole world.
15
+ #
16
+ # @api public
17
+ class ::X::Trend
18
+ include Serialization
19
+ include ValueEquality
20
+ include ValueMarshalling
21
+
22
+ # The mixins of the class by their full names, which YARD needs to resolve them in a class opened under X
23
+ #
24
+ # @!parse
25
+ # include X::Resources::Serialization
26
+ # include X::Resources::ValueEquality
27
+ # include X::Resources::ValueMarshalling
28
+
29
+ # Every trend field
30
+ #
31
+ # A minor release may add to it the fields the API adds, so that the trends of a place ask for them too.
32
+ FIELDS = %w[trend_name tweet_count].freeze
33
+ # The most trends the API returns for a place, which it returns 20 of unless asked for more
34
+ # @api private
35
+ MAX_TRENDS = 50
36
+ private_constant :MAX_TRENDS
37
+
38
+ # The raw attributes of the trend
39
+ # @api public
40
+ # @return [Hash{String => Object}] the attributes
41
+ # @example Get the raw attributes
42
+ # trend.attrs # => {"trend_name" => "#ruby", "tweet_count" => 1234}
43
+ attr_reader :attrs
44
+
45
+ # The topics trending in a place
46
+ #
47
+ # The endpoint takes app-only authentication, or OAuth 2.0 as a user, so a client that signs with OAuth 1.0a
48
+ # reads the trends with a copy that authenticates as the app.
49
+ #
50
+ # @api public
51
+ # @param woeid [Integer, String] the Yahoo! Where On Earth identifier of the place, such as 1 for the world
52
+ # @param client [Object] the client used to make the request
53
+ # @param params [Hash] query parameters, such as max_trends, which is 50, the most the API returns, unless given
54
+ # @return [Array<Trend>] the trends, frozen
55
+ # @raise [ArgumentError] if the WOEID is not a number, before a request
56
+ # @raise [InvalidAttribute] if the response holds the trends as something other than a list of objects
57
+ # @example Print the topics trending in the world
58
+ # X::Trend.at(1, client: client).each { |trend| puts trend.name }
59
+ def self.at(woeid, client:, **params)
60
+ path = "trends/by/woeid/#{woeid_of(woeid)}"
61
+ body = Utils.app_client(client).get(Utils.path(path, {"trend.fields" => FIELDS, "max_trends" => MAX_TRENDS}.merge(params)), **Utils::JSON_CLASSES)
62
+ Shape.objects("#{self}.at", body.to_h["data"]).map { |attrs| new(attrs) }.freeze
63
+ end
64
+
65
+ # The WOEID of a place, which must be a number
66
+ #
67
+ # @api private
68
+ # @param woeid [Integer, String] the WOEID
69
+ # @return [String] the WOEID
70
+ # @raise [ArgumentError] if the WOEID is not an Integer or a String of digits
71
+ def self.woeid_of(woeid)
72
+ value = case woeid
73
+ when String, Integer then woeid.to_s
74
+ end
75
+ return value if value&.match?(Utils::NUMERIC_ID)
76
+
77
+ raise ArgumentError, "#{woeid.inspect} is not a WOEID: pass an Integer, or a String of digits"
78
+ end
79
+ private_class_method :woeid_of
80
+
81
+ # Initialize a trend from the attributes the API reported
82
+ #
83
+ # @api public
84
+ # @param attrs [Hash{String, Symbol => Object}] the attributes
85
+ # @return [Trend] a new trend
86
+ # @raise [ArgumentError] if the attributes are not a Hash
87
+ # @example Build a trend
88
+ # X::Trend.new({"trend_name" => "#ruby", "tweet_count" => 1234})
89
+ def initialize(attrs)
90
+ @attrs = Utils.deep_freeze(Utils.attributes!(attrs))
91
+ freeze
92
+ end
93
+
94
+ # The name of the trend, such as a hashtag or a phrase
95
+ #
96
+ # @api public
97
+ # @return [String, nil] the name
98
+ # @example Get the name
99
+ # trend.name # => "#ruby"
100
+ def name = attrs["trend_name"]
101
+
102
+ # The number of posts about the trend
103
+ #
104
+ # It is read from post_count, or from tweet_count, the name the API gives it before it names tweets posts there,
105
+ # as the post count of a user is.
106
+ #
107
+ # @api public
108
+ # @return [Integer, nil] the number of posts, or nil if the API reported none
109
+ # @raise [InvalidAttribute] if the response holds a number of posts that is not a number
110
+ # @example Get the number of posts
111
+ # trend.post_count # => 1234
112
+ def post_count = Utils.read("#{self.class}#post_count", attrs["post_count"] || attrs["tweet_count"]) { |value| Shape.integer(value) }
113
+
114
+ alias_method :tweet_count, :post_count
115
+
116
+ # Deconstruct the trend into what its readers read, so it matches a hash pattern
117
+ #
118
+ # A pattern that asks for every key gets name and post_count, and one can ask for the number of posts as
119
+ # tweet_count too, as it can of a user.
120
+ #
121
+ # @api public
122
+ # @param keys [Array<Symbol>, nil] the keys the pattern asks for, or nil for every reader
123
+ # @return [Hash{Symbol => Object}] what the readers read
124
+ # @raise [InvalidAttribute] if the pattern asks for a number of posts the response holds as something else
125
+ # @example Keep the topics of more than 10,000 posts
126
+ # X::Trend.at(1, client: client).select { |trend| trend in {post_count: 10_000..} }
127
+ def deconstruct_keys(keys) = Utils.deconstruct(self, keys, %i[name post_count], %i[tweet_count])
128
+ end
129
+ end
130
+ end