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.
- checksums.yaml +7 -0
- data/.yardopts +9 -0
- data/CHANGELOG.md +254 -0
- data/LICENSE.txt +21 -0
- data/README.md +148 -0
- data/lib/x/resources/abstract_class.rb +29 -0
- data/lib/x/resources/actions/direct_messages.rb +99 -0
- data/lib/x/resources/actions/engagement.rb +95 -0
- data/lib/x/resources/actions/lists.rb +126 -0
- data/lib/x/resources/actions/posts.rb +77 -0
- data/lib/x/resources/actions/relationships.rb +91 -0
- data/lib/x/resources/actions.rb +22 -0
- data/lib/x/resources/api.rb +40 -0
- data/lib/x/resources/attributes.rb +203 -0
- data/lib/x/resources/batch.rb +43 -0
- data/lib/x/resources/batch_finders.rb +185 -0
- data/lib/x/resources/bookmark_folder.rb +23 -0
- data/lib/x/resources/community.rb +137 -0
- data/lib/x/resources/cursor.rb +493 -0
- data/lib/x/resources/direct_message.rb +325 -0
- data/lib/x/resources/direct_message_conversations.rb +147 -0
- data/lib/x/resources/errors.rb +104 -0
- data/lib/x/resources/finders.rb +255 -0
- data/lib/x/resources/identity.rb +65 -0
- data/lib/x/resources/includes.rb +216 -0
- data/lib/x/resources/list.rb +336 -0
- data/lib/x/resources/lookups/communities.rb +56 -0
- data/lib/x/resources/lookups/direct_messages.rb +86 -0
- data/lib/x/resources/lookups/lists.rb +44 -0
- data/lib/x/resources/lookups/media.rb +72 -0
- data/lib/x/resources/lookups/posts.rb +198 -0
- data/lib/x/resources/lookups/spaces.rb +87 -0
- data/lib/x/resources/lookups/trends.rb +38 -0
- data/lib/x/resources/lookups/users.rb +221 -0
- data/lib/x/resources/lookups.rb +25 -0
- data/lib/x/resources/marshalling.rb +93 -0
- data/lib/x/resources/matching_rule.rb +107 -0
- data/lib/x/resources/media.rb +278 -0
- data/lib/x/resources/media_ids.rb +74 -0
- data/lib/x/resources/memo.rb +54 -0
- data/lib/x/resources/page.rb +394 -0
- data/lib/x/resources/page_limit.rb +80 -0
- data/lib/x/resources/pages.rb +270 -0
- data/lib/x/resources/parallel.rb +82 -0
- data/lib/x/resources/personalized_trend.rb +124 -0
- data/lib/x/resources/place.rb +107 -0
- data/lib/x/resources/poll.rb +75 -0
- data/lib/x/resources/post.rb +615 -0
- data/lib/x/resources/post_collections.rb +67 -0
- data/lib/x/resources/post_counts.rb +215 -0
- data/lib/x/resources/post_search.rb +86 -0
- data/lib/x/resources/post_usage.rb +203 -0
- data/lib/x/resources/post_writes.rb +140 -0
- data/lib/x/resources/published_count.rb +31 -0
- data/lib/x/resources/references.rb +121 -0
- data/lib/x/resources/relation_writes.rb +54 -0
- data/lib/x/resources/relationships.rb +77 -0
- data/lib/x/resources/resource.rb +535 -0
- data/lib/x/resources/serialization.rb +58 -0
- data/lib/x/resources/shape.rb +167 -0
- data/lib/x/resources/space.rb +332 -0
- data/lib/x/resources/topic.rb +59 -0
- data/lib/x/resources/trend.rb +130 -0
- data/lib/x/resources/user.rb +502 -0
- data/lib/x/resources/user_collections.rb +213 -0
- data/lib/x/resources/user_finders.rb +282 -0
- data/lib/x/resources/utils.rb +358 -0
- data/lib/x/resources/value_equality.rb +38 -0
- data/lib/x/resources/value_marshalling.rb +89 -0
- data/lib/x/resources/version.rb +25 -0
- data/lib/x/resources.rb +22 -0
- data/sig/manifest.yaml +7 -0
- data/sig/x-resources.rbs +813 -0
- metadata +140 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "memo"
|
|
4
|
+
require_relative "page"
|
|
5
|
+
require_relative "pages"
|
|
6
|
+
require_relative "utils"
|
|
7
|
+
|
|
8
|
+
module X
|
|
9
|
+
module Resources
|
|
10
|
+
# A lazily paginated, cached, thread-safe collection of resources
|
|
11
|
+
# @api public
|
|
12
|
+
class ::X::Cursor
|
|
13
|
+
include Enumerable
|
|
14
|
+
|
|
15
|
+
# The query parameter most endpoints take the token of the next page in
|
|
16
|
+
# @api private
|
|
17
|
+
DEFAULT_TOKEN_PARAM = "pagination_token"
|
|
18
|
+
private_constant :DEFAULT_TOKEN_PARAM
|
|
19
|
+
# The message raised for a cursor serialized whole, which would read every page of its collection
|
|
20
|
+
# @api private
|
|
21
|
+
SERIALIZATION_MESSAGE = "Serializing a cursor would read every page of its collection, a billed request per " \
|
|
22
|
+
"page; serialize cursor.first(n), or cursor.to_a to read every page"
|
|
23
|
+
private_constant :SERIALIZATION_MESSAGE
|
|
24
|
+
|
|
25
|
+
# The class of the resources in this collection
|
|
26
|
+
# @api public
|
|
27
|
+
# @return [Class] the resource class
|
|
28
|
+
# @example Get the resource class
|
|
29
|
+
# user.followers.resource_class # => X::User
|
|
30
|
+
attr_reader :resource_class
|
|
31
|
+
|
|
32
|
+
# The client the resources hold, which also fetches the pages
|
|
33
|
+
#
|
|
34
|
+
# The pages of an endpoint that refuses OAuth 1.0a, such as the posts of a space, are fetched with the app-only
|
|
35
|
+
# client of a client that signs with it, while the resources hold the client itself.
|
|
36
|
+
#
|
|
37
|
+
# @api public
|
|
38
|
+
# @return [Object] the client
|
|
39
|
+
# @example Get the client
|
|
40
|
+
# cursor.client
|
|
41
|
+
attr_reader :client
|
|
42
|
+
|
|
43
|
+
# Build a cursor
|
|
44
|
+
#
|
|
45
|
+
# Internal to x-resources: a resource builds the cursors of its collections, and the searches and lookups build
|
|
46
|
+
# theirs, with it, and new is private, so that the settings a cursor pages with can change within 1.x, as the
|
|
47
|
+
# readers of the token parameter, the smallest page, and whether the pages are fetched as the app are private for
|
|
48
|
+
# the same reason. A cursor is made from another with refresh, prefetch, and stubs.
|
|
49
|
+
#
|
|
50
|
+
# @api private
|
|
51
|
+
# @param resource_class [Class] the class of the resources in the collection
|
|
52
|
+
# @param path [String] the endpoint path
|
|
53
|
+
# @param client [Object] the client the resources hold, which fetches the pages unless the endpoint takes
|
|
54
|
+
# app-only authentication
|
|
55
|
+
# @param params [Hash] query parameters merged over the resource class's default parameters
|
|
56
|
+
# @param prefetch [Boolean] whether to fetch the next page in a background thread while the current page is consumed
|
|
57
|
+
# @param token_param [String] the query parameter the token of the next page is sent in
|
|
58
|
+
# @param min_results [Integer] the smallest page the endpoint accepts, which first never asks below
|
|
59
|
+
# @param app_only [Boolean] whether the pages are fetched with the app-only client of the client, for an endpoint
|
|
60
|
+
# that refuses the OAuth 1.0a of a user, while the resources hold the client, so that they act as the user
|
|
61
|
+
# @param total [Proc, nil] a block returning the number of resources the API publishes for the collection, which
|
|
62
|
+
# reads it again when given fresh: true
|
|
63
|
+
# @param ids_only [Boolean] whether the endpoint gives the resources by their identifiers alone, and takes none of
|
|
64
|
+
# their fields, so the pages ask for none of the default parameters, and read stubs that hydrate together
|
|
65
|
+
# @return [Cursor] a new cursor
|
|
66
|
+
# @example Build a cursor over the followers of a user, which counts them with followers_count
|
|
67
|
+
# X::Cursor.__send__(:build, X::User, "users/7505382/followers", client: client, total: ->(fresh: false) { 42 })
|
|
68
|
+
# @example Build a cursor over an endpoint that pages with next_token
|
|
69
|
+
# X::Cursor.__send__(:build, X::User, "users/search", client: client, params: {query: "ruby"}, token_param: "next_token")
|
|
70
|
+
def self.build(resource_class, path, client:, params: {}, prefetch: false, token_param: DEFAULT_TOKEN_PARAM, min_results: 1, app_only: false, total: nil, ids_only: false)
|
|
71
|
+
allocate.tap { |cursor| cursor.__send__(:setup, resource_class, path, client:, params:, prefetch:, token_param:, min_results:, app_only:, total:, ids_only:) }
|
|
72
|
+
end
|
|
73
|
+
private_class_method :new, :build
|
|
74
|
+
|
|
75
|
+
# Check whether the next page is fetched in the background
|
|
76
|
+
#
|
|
77
|
+
# @api public
|
|
78
|
+
# @return [Boolean] true if pages are prefetched
|
|
79
|
+
# @example Check whether a cursor prefetches
|
|
80
|
+
# cursor.prefetch? # => false
|
|
81
|
+
def prefetch? = @prefetch
|
|
82
|
+
|
|
83
|
+
# Iterate over every resource, fetching pages as needed
|
|
84
|
+
#
|
|
85
|
+
# @api public
|
|
86
|
+
# @yield [Resource] each resource
|
|
87
|
+
# @return [Enumerator, Cursor] an enumerator without a block, otherwise self
|
|
88
|
+
# @example Print every follower
|
|
89
|
+
# user.followers.each { |follower| puts follower.username }
|
|
90
|
+
def each(&block)
|
|
91
|
+
return to_enum unless block
|
|
92
|
+
|
|
93
|
+
each_page { |page| page.each(&block) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Iterate over every page, fetching pages as needed
|
|
97
|
+
#
|
|
98
|
+
# The pages are those {#page} reads, as they were fetched, so a page need not hold as many resources as the
|
|
99
|
+
# largest page the endpoint allows.
|
|
100
|
+
#
|
|
101
|
+
# @api public
|
|
102
|
+
# @yield [Page] each page
|
|
103
|
+
# @return [Enumerator, Cursor] an enumerator without a block, otherwise self
|
|
104
|
+
# @example Print the size of every page
|
|
105
|
+
# user.followers.each_page { |page| puts page.result_count }
|
|
106
|
+
def each_page
|
|
107
|
+
return to_enum(:each_page) unless block_given?
|
|
108
|
+
|
|
109
|
+
index = 0
|
|
110
|
+
while (current = page(index))
|
|
111
|
+
yield current
|
|
112
|
+
index += 1
|
|
113
|
+
end
|
|
114
|
+
self
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Fetch a page by index, using the cache when possible
|
|
118
|
+
#
|
|
119
|
+
# The pages before the one asked for are read first, since the token of each asks for the next. A page is the
|
|
120
|
+
# page as it was fetched and kept, whatever fetched it: iterating fetches the largest page the endpoint allows,
|
|
121
|
+
# but first, take, any?, and empty? fetch pages no larger than they need, which the cursor keeps too, so that
|
|
122
|
+
# an iteration after them does not pay again for what they read. After user.followers.first, the first page
|
|
123
|
+
# holds one follower, and the pages an iteration fetches after it as many as the largest page does. The API may
|
|
124
|
+
# serve a page with fewer resources than it was asked for, or none, so no page has a size to rely on.
|
|
125
|
+
#
|
|
126
|
+
# @api public
|
|
127
|
+
# @param index [Integer] the zero-based page index
|
|
128
|
+
# @return [Page, nil] the page or nil if the collection has fewer pages
|
|
129
|
+
# @raise [TypeError] if the index is not an Integer, such as the String "1" or the Float 1.5, which name no page
|
|
130
|
+
# @raise [ArgumentError] if the index is negative, since pages are read forward from the first
|
|
131
|
+
# @example Fetch the first page
|
|
132
|
+
# user.followers.page(0)
|
|
133
|
+
def page(index) = @pages.at(index)
|
|
134
|
+
|
|
135
|
+
# Return a new cursor over the same collection with an empty page cache
|
|
136
|
+
#
|
|
137
|
+
# The number the API publishes for the collection is read again too, once, the first time published_count asks
|
|
138
|
+
# for it, since the collection it counts may have changed.
|
|
139
|
+
#
|
|
140
|
+
# @api public
|
|
141
|
+
# @return [Cursor] a new cursor
|
|
142
|
+
# @example Iterate again with fresh data
|
|
143
|
+
# followers = user.followers.refresh
|
|
144
|
+
def refresh = self.class.__send__(:build, resource_class, path, client:, params: own_params, prefetch: prefetch?, token_param:, min_results:, app_only: app_only?, total: fresh_total, ids_only: ids_only?)
|
|
145
|
+
|
|
146
|
+
# Return a new cursor over the same collection with prefetching enabled
|
|
147
|
+
#
|
|
148
|
+
# A page the background thread fails to fetch is not requested again when it is reached: the error the thread
|
|
149
|
+
# failed with is raised there, once, and a page asked for again after it is requested again.
|
|
150
|
+
#
|
|
151
|
+
# @api public
|
|
152
|
+
# @return [Cursor] a new cursor
|
|
153
|
+
# @example Fetch every follower while overlapping requests with processing
|
|
154
|
+
# user.followers.prefetch.each { |follower| process(follower) }
|
|
155
|
+
def prefetch = self.class.__send__(:build, resource_class, path, client:, params: own_params, prefetch: true, token_param:, min_results:, app_only: app_only?, total: @total, ids_only: ids_only?)
|
|
156
|
+
|
|
157
|
+
# Return a new cursor over the same collection that yields stubs
|
|
158
|
+
#
|
|
159
|
+
# The requests ask for nothing but identifiers, and each resource is a stub holding only its identifier,
|
|
160
|
+
# which hydrates on demand, even when the API returns a few default fields alongside it.
|
|
161
|
+
#
|
|
162
|
+
# @api public
|
|
163
|
+
# @return [Cursor] a new cursor
|
|
164
|
+
# @raise [UnsupportedOperation] if the resource class has no fields parameter
|
|
165
|
+
# @example Check whether a user is among thousands of followers without fetching their fields
|
|
166
|
+
# user.followers.stubs.any?(other)
|
|
167
|
+
def stubs = self.class.__send__(:build, resource_class, path, client:, params: id_only_params, prefetch: prefetch?, token_param:, min_results:, app_only: app_only?, total: @total, ids_only: true)
|
|
168
|
+
|
|
169
|
+
# The first resource, or the first few, requesting pages no larger than needed
|
|
170
|
+
#
|
|
171
|
+
# Iterating a cursor requests the largest page an endpoint allows, which costs the least in requests.
|
|
172
|
+
# The API bills each resource returned, so first asks for a page of the size it needs instead, raised to
|
|
173
|
+
# the endpoint's minimum, and each page after the first asks for no more than the pages before it left.
|
|
174
|
+
# The API may serve an empty page with the token of the next, having left out what it filters, such as
|
|
175
|
+
# suspended users, so first reads on until it finds a resource. The cursor keeps the pages first reads, as it
|
|
176
|
+
# keeps every page, so a cursor whose pages already hold what is asked for answers from them, without a request,
|
|
177
|
+
# and an iteration after first requests only what first left.
|
|
178
|
+
#
|
|
179
|
+
# @api public
|
|
180
|
+
# @param count [Integer, nil] the number of resources, or nil for the first resource alone; a Float is read as
|
|
181
|
+
# the Integer it converts to, as Array#first reads it
|
|
182
|
+
# @return [Resource, Array<Resource>, nil] the first resource, or the first resources, frozen
|
|
183
|
+
# @raise [ArgumentError] if the count is negative
|
|
184
|
+
# @raise [TypeError] if the count is not a number that converts to an Integer
|
|
185
|
+
# @example Read ten followers in one request for ten users
|
|
186
|
+
# user.followers.first(10)
|
|
187
|
+
def first(count = nil)
|
|
188
|
+
resources = @pages.read(count.nil? ? 1 : Utils.count!(count)) #: Array[untyped]
|
|
189
|
+
return resources unless count.nil?
|
|
190
|
+
|
|
191
|
+
resource, = resources
|
|
192
|
+
resource
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Every resource, fetching every page
|
|
196
|
+
#
|
|
197
|
+
# The array is frozen, as the arrays first and take return are, since a cursor keeps the pages it read and a
|
|
198
|
+
# caller that changed what it returned would change nothing the cursor holds.
|
|
199
|
+
#
|
|
200
|
+
# @api public
|
|
201
|
+
# @return [Array<Resource>] every resource, frozen
|
|
202
|
+
# @example Read every follower
|
|
203
|
+
# user.followers.to_a
|
|
204
|
+
def to_a = super.freeze
|
|
205
|
+
|
|
206
|
+
alias_method :entries, :to_a
|
|
207
|
+
|
|
208
|
+
# The first few resources, requesting pages no larger than needed, as first does
|
|
209
|
+
#
|
|
210
|
+
# @api public
|
|
211
|
+
# @param count [Integer] the number of resources; a Float is read as the Integer it converts to, as Array#take
|
|
212
|
+
# reads it
|
|
213
|
+
# @return [Array<Resource>] the first resources, frozen
|
|
214
|
+
# @raise [ArgumentError] if the count is negative
|
|
215
|
+
# @raise [TypeError] if the count is not a number that converts to an Integer, such as nil or a String
|
|
216
|
+
# @example Read three followers in one request for three users
|
|
217
|
+
# user.followers.take(3)
|
|
218
|
+
def take(count) = first(Utils.count!(count))
|
|
219
|
+
|
|
220
|
+
# Check whether the collection holds any resource, requesting one
|
|
221
|
+
#
|
|
222
|
+
# Without a pattern or a block, this asks for a single resource, raised to the endpoint's minimum, rather than a
|
|
223
|
+
# full page.
|
|
224
|
+
#
|
|
225
|
+
# @api public
|
|
226
|
+
# @param pattern [Object] a pattern each resource is matched against
|
|
227
|
+
# @yield [Resource] each resource
|
|
228
|
+
# @return [Boolean] true if any resource matches
|
|
229
|
+
# @example Check whether a user has any followers
|
|
230
|
+
# user.followers.any?
|
|
231
|
+
def any?(*pattern, &block)
|
|
232
|
+
return super unless pattern.empty? && block.nil?
|
|
233
|
+
|
|
234
|
+
!first.nil?
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Check whether the collection holds no resource, requesting one
|
|
238
|
+
#
|
|
239
|
+
# Without a pattern or a block, this asks for a single resource, raised to the endpoint's minimum, rather than a
|
|
240
|
+
# full page.
|
|
241
|
+
#
|
|
242
|
+
# @api public
|
|
243
|
+
# @param pattern [Object] a pattern each resource is matched against
|
|
244
|
+
# @yield [Resource] each resource
|
|
245
|
+
# @return [Boolean] true if no resource matches
|
|
246
|
+
# @example Check whether a user follows nobody
|
|
247
|
+
# user.following.none?
|
|
248
|
+
def none?(*pattern, &block)
|
|
249
|
+
return super unless pattern.empty? && block.nil?
|
|
250
|
+
|
|
251
|
+
first.nil?
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Check whether the collection is empty, requesting one resource
|
|
255
|
+
#
|
|
256
|
+
# Like none? without a pattern or a block, this asks for a single resource, raised to the endpoint's minimum,
|
|
257
|
+
# rather than a full page.
|
|
258
|
+
#
|
|
259
|
+
# @api public
|
|
260
|
+
# @return [Boolean] true if the collection holds no resource
|
|
261
|
+
# @example Check whether a user has no followers
|
|
262
|
+
# user.followers.empty?
|
|
263
|
+
def empty? = first.nil?
|
|
264
|
+
|
|
265
|
+
# Check whether the collection holds one resource, requesting two
|
|
266
|
+
#
|
|
267
|
+
# Without a pattern or a block, this asks for two resources, raised to the endpoint's minimum, rather than a full
|
|
268
|
+
# page.
|
|
269
|
+
#
|
|
270
|
+
# @api public
|
|
271
|
+
# @param pattern [Object] a pattern each resource is matched against
|
|
272
|
+
# @yield [Resource] each resource
|
|
273
|
+
# @return [Boolean] true if exactly one resource matches
|
|
274
|
+
# @example Check whether a list has a single member
|
|
275
|
+
# list.members.one?
|
|
276
|
+
def one?(*pattern, &block)
|
|
277
|
+
return super unless pattern.empty? && block.nil?
|
|
278
|
+
|
|
279
|
+
take(2).size.eql?(1)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# The number the API publishes for the collection, without reading any of it
|
|
283
|
+
#
|
|
284
|
+
# The API publishes a number for a user's followers, followed users, and list memberships, and for a list's
|
|
285
|
+
# members and followers. Reading it costs no request when the user or list holds it, and one lookup when it is a
|
|
286
|
+
# stub. The number counts what the collection holds, which can differ from what count reads, since the
|
|
287
|
+
# endpoint leaves out what the authenticated user cannot see, such as private lists and suspended users. count
|
|
288
|
+
# instead reads every page of the collection, a request per page, and the API bills each resource. A cursor
|
|
289
|
+
# answers no size, so that Ruby's own methods, such as each_slice and lazy, do not page a collection to size it.
|
|
290
|
+
#
|
|
291
|
+
# @api public
|
|
292
|
+
# @return [Integer, nil] the published number, or nil for a collection the API publishes no number for
|
|
293
|
+
# @example Count a user's followers without reading one of them
|
|
294
|
+
# user.followers.published_count # => 12345
|
|
295
|
+
def published_count = @total&.call
|
|
296
|
+
|
|
297
|
+
# The identifiers of every resource, requesting nothing but identifiers
|
|
298
|
+
#
|
|
299
|
+
# @api public
|
|
300
|
+
# @return [Array<Integer, String>] the identifiers, Integers unless the resource's identifiers are not numbers,
|
|
301
|
+
# frozen
|
|
302
|
+
# @raise [UnsupportedOperation] if the resource class has no fields parameter
|
|
303
|
+
# @example Get the identifiers of every follower
|
|
304
|
+
# user.followers.ids
|
|
305
|
+
def ids = stubs.map(&:id).freeze
|
|
306
|
+
|
|
307
|
+
# Refuse to write the collection as JSON, which would read every page of it
|
|
308
|
+
#
|
|
309
|
+
# Serializing a cursor would read every page of the collection, a request per page, and the API bills each
|
|
310
|
+
# resource it returns, from a call that says nothing of it, such as a cursor in a Hash that a log or a render
|
|
311
|
+
# writes. It raises instead, as ActiveSupport would otherwise read a cursor as the Enumerable it is. Serialize what
|
|
312
|
+
# first(n) or to_a reads instead, each of which says at the call how much it reads.
|
|
313
|
+
#
|
|
314
|
+
# @api public
|
|
315
|
+
# @return [void]
|
|
316
|
+
# @raise [UnsupportedOperation] always
|
|
317
|
+
# @example Serialize the first ten followers rather than every one of them
|
|
318
|
+
# user.followers.first(10).as_json
|
|
319
|
+
def as_json(*) = raise(UnsupportedOperation, SERIALIZATION_MESSAGE)
|
|
320
|
+
|
|
321
|
+
# A Hash of the pair a block returns for each resource, reading every page
|
|
322
|
+
#
|
|
323
|
+
# Without a block it raises as {#as_json} does, before it reads a page, since a page of the collection is read as
|
|
324
|
+
# a Hash only by its as_json, rather than raise TypeError from the to_h of Enumerable once it has read one.
|
|
325
|
+
#
|
|
326
|
+
# @api public
|
|
327
|
+
# @yieldparam resource [Resource] each resource
|
|
328
|
+
# @yieldreturn [Array(Object, Object)] the key and value of the resource
|
|
329
|
+
# @return [Hash] the pairs the block returns
|
|
330
|
+
# @raise [UnsupportedOperation] if no block is given
|
|
331
|
+
# @example Index the members of a list by username
|
|
332
|
+
# list.members.to_h { |user| [user.username, user] }
|
|
333
|
+
def to_h(&block) = block ? super() : raise(UnsupportedOperation, SERIALIZATION_MESSAGE)
|
|
334
|
+
|
|
335
|
+
# Refuse to write the collection as a JSON array, which would read every page
|
|
336
|
+
#
|
|
337
|
+
# It raises as {#as_json} does, for the reason that says.
|
|
338
|
+
#
|
|
339
|
+
# @api public
|
|
340
|
+
# @param _state [JSON::State, nil] the state a JSON encoder passes
|
|
341
|
+
# @return [void]
|
|
342
|
+
# @raise [UnsupportedOperation] always
|
|
343
|
+
# @example Serialize a whole collection, reading every page of it
|
|
344
|
+
# list.members.to_a.to_json # => "[{\"id\":\"7505382\"}]"
|
|
345
|
+
def to_json(_state = nil) = raise(UnsupportedOperation, SERIALIZATION_MESSAGE)
|
|
346
|
+
|
|
347
|
+
# Refuse to write the cursor with Marshal, as it refuses to write it as JSON
|
|
348
|
+
#
|
|
349
|
+
# A cursor holds its client, and the threads and locks that fetch its pages, none of which Marshal can write, and
|
|
350
|
+
# caching the collection it names would mean reading every page of it, as {#as_json} says. It raises the
|
|
351
|
+
# TypeError Marshal raises for what it cannot write, with the message of {#as_json}, rather than the one Marshal
|
|
352
|
+
# would raise from within the cursor. Marshal what first(n) or to_a reads, or a page, instead.
|
|
353
|
+
#
|
|
354
|
+
# @api public
|
|
355
|
+
# @return [void]
|
|
356
|
+
# @raise [TypeError] always
|
|
357
|
+
# @example Cache the first page of the followers of a user rather than the cursor
|
|
358
|
+
# Rails.cache.write("followers", user.followers.page(0))
|
|
359
|
+
def marshal_dump = raise(TypeError, SERIALIZATION_MESSAGE)
|
|
360
|
+
|
|
361
|
+
# Refuse to write the cursor as YAML, as it refuses Marshal
|
|
362
|
+
#
|
|
363
|
+
# YAML reads no marshal_dump, and would write every instance variable of the cursor, its client and the
|
|
364
|
+
# credentials it holds among them, so it raises as {#marshal_dump} does. Write what first(n) or to_a reads, or a
|
|
365
|
+
# page, instead.
|
|
366
|
+
#
|
|
367
|
+
# @api public
|
|
368
|
+
# @param _coder [Psych::Coder] the coder YAML would write the cursor with
|
|
369
|
+
# @return [void]
|
|
370
|
+
# @raise [TypeError] always
|
|
371
|
+
# @example Write the first page of the followers of a user as YAML rather than the cursor
|
|
372
|
+
# YAML.dump(user.followers.page(0))
|
|
373
|
+
def encode_with(_coder) = raise(TypeError, SERIALIZATION_MESSAGE)
|
|
374
|
+
|
|
375
|
+
# Summarize the cursor for the console
|
|
376
|
+
#
|
|
377
|
+
# @api public
|
|
378
|
+
# @return [String] the class name, resource class, and path
|
|
379
|
+
# @example Inspect a cursor
|
|
380
|
+
# user.followers.inspect # => #<X::Cursor resource_class=X::User path="users/7505382/followers">
|
|
381
|
+
def inspect = "#<#{self.class} resource_class=#{resource_class} path=#{path.inspect}>"
|
|
382
|
+
|
|
383
|
+
private
|
|
384
|
+
|
|
385
|
+
# Set the collection, requests, and pages of a new cursor, and freeze it
|
|
386
|
+
# @api private
|
|
387
|
+
# @return [void]
|
|
388
|
+
def setup(resource_class, path, client:, params:, prefetch:, token_param:, min_results:, app_only:, total:, ids_only:)
|
|
389
|
+
@resource_class = resource_class
|
|
390
|
+
@client = client
|
|
391
|
+
@path = path
|
|
392
|
+
@params = Utils.deep_freeze(Utils.merge_params(ids_only ? {} : resource_class.default_params, params))
|
|
393
|
+
@prefetch, @app_only, @ids_only = prefetch, app_only, ids_only
|
|
394
|
+
@token_param = token_param
|
|
395
|
+
@min_results = min_results
|
|
396
|
+
@total = total
|
|
397
|
+
@pages = Pages.new(self)
|
|
398
|
+
freeze
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# The endpoint path
|
|
402
|
+
#
|
|
403
|
+
# Internal to x-resources: the pages of a cursor are requested at it, and the endpoint a collection is read from may
|
|
404
|
+
# change within 1.x, as the API moves one.
|
|
405
|
+
#
|
|
406
|
+
# @api private
|
|
407
|
+
# @return [String] the endpoint path
|
|
408
|
+
# @example Get the path
|
|
409
|
+
# user.followers.__send__(:path) # => "users/7505382/followers"
|
|
410
|
+
attr_reader :path
|
|
411
|
+
|
|
412
|
+
# The query parameters sent with every page request
|
|
413
|
+
#
|
|
414
|
+
# Internal to x-resources: the pages of a cursor are requested with them, and they hold the default fields of the
|
|
415
|
+
# resource class, which a minor release may add to, and the size of a page, which it may change.
|
|
416
|
+
#
|
|
417
|
+
# @api private
|
|
418
|
+
# @return [Hash{String => Object}] the query parameters
|
|
419
|
+
# @example Get the parameters
|
|
420
|
+
# user.followers.__send__(:params)["max_results"] # => 1000
|
|
421
|
+
attr_reader :params
|
|
422
|
+
|
|
423
|
+
# The smallest page the endpoint accepts
|
|
424
|
+
#
|
|
425
|
+
# Internal to x-resources: Pages never asks for a smaller page than this.
|
|
426
|
+
#
|
|
427
|
+
# @api private
|
|
428
|
+
# @return [Integer] the minimum page size
|
|
429
|
+
attr_reader :min_results
|
|
430
|
+
|
|
431
|
+
# The query parameter the token of the next page is sent in
|
|
432
|
+
#
|
|
433
|
+
# Internal to x-resources: Pages sends the token of each page after the first in it.
|
|
434
|
+
#
|
|
435
|
+
# @api private
|
|
436
|
+
# @return [String] the parameter name
|
|
437
|
+
attr_reader :token_param
|
|
438
|
+
|
|
439
|
+
# Check whether the pages are fetched with the app-only client of the client
|
|
440
|
+
#
|
|
441
|
+
# The space endpoints refuse the OAuth 1.0a of a user, so a cursor over one fetches its pages with the app-only
|
|
442
|
+
# client, while its resources hold the client, so that they act as the user. Internal to x-resources: Pages asks
|
|
443
|
+
# it which client fetches a page.
|
|
444
|
+
#
|
|
445
|
+
# @api private
|
|
446
|
+
# @return [Boolean] true if pages are fetched as the app
|
|
447
|
+
def app_only? = @app_only
|
|
448
|
+
|
|
449
|
+
# Check whether the endpoint gives the resources by their identifiers alone
|
|
450
|
+
#
|
|
451
|
+
# Such an endpoint takes none of their fields either. Internal to x-resources: the pages of such a cursor read stubs, which is what Pages asks it for this.
|
|
452
|
+
#
|
|
453
|
+
# @api private
|
|
454
|
+
# @return [Boolean] true if the pages ask for none of the default parameters, and read stubs
|
|
455
|
+
def ids_only? = @ids_only
|
|
456
|
+
|
|
457
|
+
# The block of a refreshed cursor, which reads the published number again once
|
|
458
|
+
# @api private
|
|
459
|
+
# @return [Proc, nil] the block, or nil for a collection the API publishes no number for
|
|
460
|
+
def fresh_total
|
|
461
|
+
total = @total or return
|
|
462
|
+
count = Memo.new
|
|
463
|
+
lambda do |fresh: false|
|
|
464
|
+
# @type var fresh: bool
|
|
465
|
+
fresh ? total.call(fresh: true) : count.fetch { total.call(fresh: true) }
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# The parameters of this cursor, keeping dropped defaults dropped
|
|
470
|
+
# @api private
|
|
471
|
+
# @return [Hash{String => Object}] the parameters
|
|
472
|
+
def own_params
|
|
473
|
+
dropped = {} #: Hash[String, nil]
|
|
474
|
+
resource_class.default_params.each_key { |key| dropped[key] = nil }
|
|
475
|
+
dropped.merge(params)
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# The query parameters that select nothing but the identifier
|
|
479
|
+
# @api private
|
|
480
|
+
# @return [Hash{String => Object}] the query parameters
|
|
481
|
+
# @raise [UnsupportedOperation] if the resource class has no fields parameter
|
|
482
|
+
def id_only_params
|
|
483
|
+
return params if ids_only?
|
|
484
|
+
|
|
485
|
+
fields_key = resource_class.__send__(:fields_key) || raise(UnsupportedOperation, "#{resource_class} has no fields parameter") #: String
|
|
486
|
+
id_key = resource_class.__send__(:id_key) #: String
|
|
487
|
+
dropped = {} #: Hash[String, nil]
|
|
488
|
+
resource_class.default_params.each_key { |key| dropped[key] = nil }
|
|
489
|
+
params.merge(dropped, fields_key => id_key)
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
end
|