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,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "bookmark_folder"
4
+ require_relative "utils"
5
+
6
+ module X
7
+ module Resources
8
+ # The collections of a user: the users they follow and are followed by, their posts, timelines, bookmarks, and
9
+ # lists, included into User
10
+ #
11
+ # Internal to x-resources: the methods it gives a user, such as followers, are public API, but the module is only how
12
+ # they are shared, and which classes extend or include it can change within 1.x.
13
+ #
14
+ # @api semipublic
15
+ module UserCollections
16
+ # Maximum number of users per page of the followers, followed users, affiliates, blocked users, or muted users
17
+ # @api private
18
+ MAX_FOLLOW_RESULTS = 1000
19
+ # Maximum number of posts, lists, or bookmark folders per page of the other collections of a user
20
+ # @api private
21
+ MAX_RESULTS = 100
22
+ private_constant :MAX_FOLLOW_RESULTS, :MAX_RESULTS
23
+
24
+ # The users following this user
25
+ #
26
+ # @api public
27
+ # @param params [Hash] query parameters merged over the default parameters
28
+ # @return [Cursor] a cursor over the followers
29
+ # @example Print every follower
30
+ # user.followers.each { |follower| puts follower.username }
31
+ def followers(**params)
32
+ cursor(User, "users/#{id}/followers", max_results: MAX_FOLLOW_RESULTS, total: :followers_count, **params)
33
+ end
34
+
35
+ # The users this user follows
36
+ #
37
+ # @api public
38
+ # @param params [Hash] query parameters merged over the default parameters
39
+ # @return [Cursor] a cursor over the followed users
40
+ # @example Count the followed users
41
+ # user.following.count
42
+ def following(**params)
43
+ cursor(User, "users/#{id}/following", max_results: MAX_FOLLOW_RESULTS, total: :following_count, **params)
44
+ end
45
+
46
+ # The users affiliated with this user, such as the people of an organization
47
+ #
48
+ # They are the accounts whose affiliation names this user, which affiliated_with reads of each of them, so they
49
+ # point the other way from the affiliated_with of this user.
50
+ #
51
+ # @api public
52
+ # @param params [Hash] query parameters merged over the default parameters
53
+ # @return [Cursor] a cursor over the users affiliated with this one
54
+ # @example Print the users affiliated with an organization
55
+ # client.find_user!("X").affiliates.each { |user| puts user.username }
56
+ def affiliates(**params) = cursor(User, "users/#{id}/affiliates", max_results: MAX_FOLLOW_RESULTS, **params)
57
+
58
+ # The users this user blocks, which must be the authenticated user
59
+ #
60
+ # @api public
61
+ # @param params [Hash] query parameters merged over the default parameters
62
+ # @return [Cursor] a cursor over the blocked users
63
+ # @example Print every blocked user
64
+ # client.current_user!.blocking.each { |user| puts user.username }
65
+ def blocking(**params)
66
+ cursor(User, "users/#{id}/blocking", max_results: MAX_FOLLOW_RESULTS, **params)
67
+ end
68
+
69
+ # The users this user mutes, which must be the authenticated user
70
+ #
71
+ # @api public
72
+ # @param params [Hash] query parameters merged over the default parameters
73
+ # @return [Cursor] a cursor over the muted users
74
+ # @example Print every muted user
75
+ # client.current_user!.muting.each { |user| puts user.username }
76
+ def muting(**params)
77
+ cursor(User, "users/#{id}/muting", max_results: MAX_FOLLOW_RESULTS, **params)
78
+ end
79
+
80
+ # The posts by this user
81
+ #
82
+ # @api public
83
+ # @param params [Hash] query parameters merged over the default parameters
84
+ # @return [Cursor] a cursor over the posts
85
+ # @example Print the most recent posts
86
+ # user.posts.first(10).each { |post| puts post.text }
87
+ def posts(**params)
88
+ cursor(Post, "users/#{id}/tweets", max_results: MAX_RESULTS, min_results: 5, **params)
89
+ end
90
+
91
+ # The home timeline of this user, which must be the authenticated user
92
+ #
93
+ # @api public
94
+ # @param params [Hash] query parameters merged over the default parameters
95
+ # @return [Cursor] a cursor over the posts by the users this user follows, newest first
96
+ # @example Print the home timeline
97
+ # client.current_user!.home_timeline.first(10).each { |post| puts post.text }
98
+ def home_timeline(**params)
99
+ cursor(Post, "users/#{id}/timelines/reverse_chronological", max_results: MAX_RESULTS, **params)
100
+ end
101
+
102
+ # The posts mentioning this user
103
+ #
104
+ # @api public
105
+ # @param params [Hash] query parameters merged over the default parameters
106
+ # @return [Cursor] a cursor over the mentions
107
+ # @example Print the most recent mentions
108
+ # user.mentions.first(10).each { |post| puts post.text }
109
+ def mentions(**params)
110
+ cursor(Post, "users/#{id}/mentions", max_results: MAX_RESULTS, min_results: 5, **params)
111
+ end
112
+
113
+ # The posts liked by this user
114
+ #
115
+ # @api public
116
+ # @param params [Hash] query parameters merged over the default parameters
117
+ # @return [Cursor] a cursor over the liked posts
118
+ # @example Print the most recently liked posts
119
+ # user.liked_posts.first(10).each { |post| puts post.text }
120
+ def liked_posts(**params)
121
+ cursor(Post, "users/#{id}/liked_tweets", max_results: MAX_RESULTS, min_results: 5, **params)
122
+ end
123
+
124
+ # The posts bookmarked by the authenticated user, or those of one of their folders
125
+ #
126
+ # The API gives the posts of a folder by their identifiers alone, and takes no fields for them, so they are
127
+ # stubs, which hydrate together, a lookup's worth at a time, as the stubs of any cursor do.
128
+ #
129
+ # The bookmark endpoints take only the authentication of a user, OAuth 2.0 user context or OAuth 1.0a, so an
130
+ # app-only client is refused them; only bookmarking and unbookmarking a post refuse OAuth 1.0a too.
131
+ #
132
+ # @api public
133
+ # @param folder [BookmarkFolder, String, Integer, nil] the folder or its identifier, or nil for every bookmark
134
+ # @param params [Hash] query parameters merged over the default parameters
135
+ # @return [Cursor] a cursor over the bookmarked posts
136
+ # @raise [ArgumentError] if the folder is not a folder or the identifier of one, before a request
137
+ # @example Print the bookmarked posts
138
+ # client.current_user!.bookmarks.each { |post| puts post.text }
139
+ # @example Print the posts of the first bookmark folder
140
+ # user = client.current_user!
141
+ # if (folder = user.bookmark_folders.first)
142
+ # posts = user.bookmarks(folder:).to_a
143
+ # X::Post.hydrate_all(posts, client: client).each { |post| puts post.text }
144
+ # end
145
+ def bookmarks(folder: nil, **params)
146
+ return cursor(Post, "users/#{id}/bookmarks", max_results: MAX_RESULTS, **params) if folder.nil?
147
+
148
+ cursor(Post, "users/#{id}/bookmarks/folders/#{Utils.id_of(folder, BookmarkFolder)}", max_results: MAX_RESULTS, ids_only: true, **params)
149
+ end
150
+
151
+ # The bookmark folders of the authenticated user
152
+ #
153
+ # @api public
154
+ # @param params [Hash] query parameters merged over the default parameters
155
+ # @return [Cursor] a cursor over the folders
156
+ # @example Print the names of the bookmark folders
157
+ # client.current_user!.bookmark_folders.each { |folder| puts folder.name }
158
+ def bookmark_folders(**params)
159
+ cursor(BookmarkFolder, "users/#{id}/bookmarks/folders", max_results: MAX_RESULTS, **params)
160
+ end
161
+
162
+ # The lists owned by this user
163
+ #
164
+ # @api public
165
+ # @param params [Hash] query parameters merged over the default parameters
166
+ # @return [Cursor] a cursor over the owned lists
167
+ # @example Print the owned lists
168
+ # user.owned_lists.each { |list| puts list.name }
169
+ def owned_lists(**params)
170
+ cursor(List, "users/#{id}/owned_lists", max_results: MAX_RESULTS, **params)
171
+ end
172
+
173
+ # The lists this user is a member of
174
+ #
175
+ # @api public
176
+ # @param params [Hash] query parameters merged over the default parameters
177
+ # @return [Cursor] a cursor over the list memberships
178
+ # @example Print the list memberships
179
+ # user.list_memberships.each { |list| puts list.name }
180
+ def list_memberships(**params)
181
+ cursor(List, "users/#{id}/list_memberships", max_results: MAX_RESULTS, total: :listed_count, **params)
182
+ end
183
+
184
+ # The lists this user follows
185
+ #
186
+ # @api public
187
+ # @param params [Hash] query parameters merged over the default parameters
188
+ # @return [Cursor] a cursor over the followed lists
189
+ # @example Print the followed lists
190
+ # user.followed_lists.each { |list| puts list.name }
191
+ def followed_lists(**params)
192
+ cursor(List, "users/#{id}/followed_lists", max_results: MAX_RESULTS, **params)
193
+ end
194
+
195
+ # The lists this user has pinned
196
+ #
197
+ # The API returns them in one response, without pages.
198
+ #
199
+ # @api public
200
+ # @param params [Hash] query parameters merged over the default parameters
201
+ # @return [Cursor] a cursor over the pinned lists
202
+ # @example Print the pinned lists
203
+ # client.current_user!.pinned_lists.each { |list| puts list.name }
204
+ def pinned_lists(**params)
205
+ cursor(List, "users/#{id}/pinned_lists", max_results: nil, **params)
206
+ end
207
+
208
+ alias_method :tweets, :posts
209
+ alias_method :liked_tweets, :liked_posts
210
+ end
211
+ private_constant :UserCollections
212
+ end
213
+ end
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "batch_finders"
5
+ require_relative "utils"
6
+
7
+ module X
8
+ module Resources
9
+ # Class methods that look users up by identifier or username, and the authenticated user, extended into User
10
+ #
11
+ # Internal to x-resources: the methods it gives User, such as X::User.find_by_username, are public API, but the module
12
+ # is only how they are shared, and which classes extend or include it can change within 1.x.
13
+ #
14
+ # @api semipublic
15
+ module UserFinders
16
+ include BatchFinders
17
+
18
+ # Look up a user by identifier or username
19
+ #
20
+ # An Integer or a user is looked up by identifier, and a String by username, with or without a leading at sign.
21
+ #
22
+ # @api public
23
+ # @param id_or_username [Integer, User, String] an identifier or a user, or a username
24
+ # @param client [Object] the client used to make the request
25
+ # @param params [Hash] query parameters merged over the default parameters
26
+ # @return [User, nil] the user or nil if the user was not found, whether the API answers 200 with no data or a 404
27
+ # that reports the user as not found
28
+ # @raise [X::NotFound] if the API answers any other 404, such as one from a client pointed at the wrong host or
29
+ # API version
30
+ # @raise [ArgumentError] if the value is neither an identifier nor a username
31
+ # @yieldparam problem [Problem] each problem the API reported, such as a user that was not found
32
+ # @example Look up a user by username
33
+ # X::User.find("sferik", client: client)
34
+ def find(id_or_username, client:, **params, &)
35
+ return super if Utils.id?(id_or_username)
36
+
37
+ find_by_username(_ = id_or_username, client:, **params, &)
38
+ end
39
+
40
+ # Look up a user by identifier or username, which must exist
41
+ #
42
+ # An Integer or a user is looked up by identifier, and a String by username, as find looks them up, so the error
43
+ # it raises names a username as find_by_username! names one, with the at sign of a handle.
44
+ #
45
+ # @api public
46
+ # @param id_or_username [Integer, User, String] an identifier or a user, or a username
47
+ # @param client [Object] the client used to make the request
48
+ # @param params [Hash] query parameters merged over the default parameters
49
+ # @return [User] the user
50
+ # @raise [ArgumentError] if the value is neither an identifier nor a username
51
+ # @raise [MissingResource] if the user was not found, whether the API answers 200 with no data or a 404 that
52
+ # reports the user as not found
53
+ # @raise [X::NotFound] if the API answers any other 404, such as one from a client pointed at the wrong host or
54
+ # API version
55
+ # @example Look up a user by username
56
+ # X::User.find!("sferik", client: client)
57
+ def find!(id_or_username, client:, **params)
58
+ return super if Utils.id?(id_or_username)
59
+
60
+ find_by_username!(_ = id_or_username, client:, **params)
61
+ end
62
+
63
+ # Look up many users by identifier or username, in parallel batches
64
+ #
65
+ # Integers and users are looked up by identifier, and Strings by username. The users come back in the
66
+ # order they were asked for, one for each value that was found, so a user asked for twice, by identifier or by
67
+ # username, comes back twice, without the ones that were not found. Every username is checked before any
68
+ # request, so one that is not a username raises before the identifiers are looked up, and billed.
69
+ #
70
+ # @api public
71
+ # @param ids_or_usernames [Array<Integer, User, String>] identifiers or users, or usernames
72
+ # @param client [Object] the client used to make the requests
73
+ # @param concurrency [Integer] the number of batches looked up at once, which must be at least one; the
74
+ # identifiers and the usernames are looked up one kind after the other, each kind that many batches at a time
75
+ # @param params [Hash] query parameters merged over the default parameters
76
+ # @return [Array<User>] the users that were found, one for each value that names one, frozen
77
+ # @raise [ArgumentError] if a String is not a username, or if the concurrency is less than one, before any request
78
+ # @yieldparam problem [Problem] each problem the API reported, such as a user that was not found
79
+ # @example Look up many users by username
80
+ # X::User.find_all(["sferik", "gem"], client: client)
81
+ def find_all(ids_or_usernames, client:, concurrency: DEFAULT_CONCURRENCY, **params, &)
82
+ ids, usernames = ids_or_usernames.partition { |value| Utils.id?(value) }
83
+ usernames.each { |username| Utils.username!(username) }
84
+ found = super(ids, client:, concurrency:, **params) #: Array[User]
85
+ in_order(found + find_all_by_username(_ = usernames, client:, concurrency:, **params, &), ids_or_usernames)
86
+ end
87
+
88
+ # Look up many users by identifier, in parallel batches
89
+ #
90
+ # A String of digits is an identifier, as it is read from a response or an environment variable, so this looks
91
+ # the accounts those numbers identify up, where find_all would take them for usernames. The users come back in
92
+ # the order they were asked for, one for each identifier that was found, as find_all returns them.
93
+ #
94
+ # @api public
95
+ # @param ids [Array<String, Integer, User>] the identifiers, or users
96
+ # @param client [Object] the client used to make the requests
97
+ # @param concurrency [Integer] the number of batches looked up at once, which must be at least one
98
+ # @param params [Hash] query parameters merged over the default parameters
99
+ # @return [Array<User>] the users that were found, one for each identifier that names one, frozen
100
+ # @raise [ArgumentError] if a value is not an identifier, or if the concurrency is less than one
101
+ # @yieldparam problem [Problem] each problem the API reported, such as an identifier that was not found
102
+ # @example Look up many users by identifier, read as Strings
103
+ # X::User.find_all_by_id(ENV.fetch("USER_IDS").split(","), client: client)
104
+ def find_all_by_id(ids, client:, concurrency: DEFAULT_CONCURRENCY, **params, &)
105
+ ids = ids.map { |id| Utils.id_of(id, self) }
106
+ in_order_of(lookup_in_batches(endpoint!, batch_key, ids, client:, concurrency:, **params, &), ids) #: Array[User]
107
+ end
108
+
109
+ # Look up a user by identifier
110
+ #
111
+ # A String of digits is an identifier, as it is read from a response or an environment variable, so this looks
112
+ # the account that number identifies up, where find would take it for a username.
113
+ #
114
+ # @api public
115
+ # @param id [String, Integer, User] the identifier, or a user
116
+ # @param client [Object] the client used to make the request
117
+ # @param params [Hash] query parameters merged over the default parameters
118
+ # @return [User, nil] the user or nil if the user was not found, whether the API answers 200 with no data or a 404
119
+ # that reports the user as not found
120
+ # @raise [X::NotFound] if the API answers any other 404, such as one from a client pointed at the wrong host or
121
+ # API version
122
+ # @raise [ArgumentError] if the value is not an identifier
123
+ # @yieldparam problem [Problem] each problem the API reported, such as a user that was not found
124
+ # @example Look up a user by an identifier read as a String
125
+ # X::User.find_by_id(ENV.fetch("USER_ID"), client: client)
126
+ def find_by_id(id, client:, **params, &)
127
+ locate("#{endpoint!}/#{Utils.id_of(id, self)}", client:, **params, &) #: User?
128
+ end
129
+
130
+ # Look up a user by identifier, which must exist
131
+ #
132
+ # @api public
133
+ # @param id [String, Integer, User] the identifier, or a user
134
+ # @param client [Object] the client used to make the request
135
+ # @param params [Hash] query parameters merged over the default parameters
136
+ # @return [User] the user
137
+ # @raise [ArgumentError] if the value is not an identifier
138
+ # @raise [MissingResource] if the user was not found, whether the API answers 200 with no data or a 404 that
139
+ # reports the user as not found
140
+ # @raise [X::NotFound] if the API answers any other 404, such as one from a client pointed at the wrong host or
141
+ # API version
142
+ # @example Look up a user by an identifier read as a String
143
+ # X::User.find_by_id!("7505382", client: client)
144
+ def find_by_id!(id, client:, **params)
145
+ name = Utils.id_of(id, self)
146
+ locate!("#{endpoint!}/#{name}", name, client:, **params) #: User
147
+ end
148
+
149
+ # Look up many users by username, in parallel batches
150
+ #
151
+ # The users come back in the order they were asked for, one for each username that was found, whatever its
152
+ # case, as find_all returns them, and each username is asked for once, however often, and in whatever case, it
153
+ # is given.
154
+ #
155
+ # @api public
156
+ # @param usernames [Array<String>] the usernames, with or without leading at signs
157
+ # @param client [Object] the client used to make the requests
158
+ # @param concurrency [Integer] the number of batches looked up at once, which must be at least one
159
+ # @param params [Hash] query parameters merged over the default parameters
160
+ # @return [Array<User>] the users that were found, one for each username that names one, frozen
161
+ # @raise [ArgumentError] if a value is not a username, or if the concurrency is less than one
162
+ # @yieldparam problem [Problem] each problem the API reported, such as a username that was not found
163
+ # @example Look up many users by username
164
+ # X::User.find_all_by_username(["sferik", "gem"], client: client)
165
+ def find_all_by_username(usernames, client:, concurrency: DEFAULT_CONCURRENCY, **params, &)
166
+ usernames = usernames.map { |username| normalize(Utils.username!(username)) }
167
+ found = lookup_in_batches("users/by", :usernames, usernames, client:, concurrency:, **params, &) #: Array[User]
168
+ in_order(found, usernames)
169
+ end
170
+
171
+ # Look up a user by username
172
+ #
173
+ # It looks every value up as a username, a String of digits as the account whose handle is that number, as find
174
+ # looks up any String, so code that reads a value from elsewhere says which it means, as find_by_id does.
175
+ #
176
+ # @api public
177
+ # @param username [String] the username, with or without a leading at sign
178
+ # @param client [Object] the client used to make the request
179
+ # @param params [Hash] query parameters merged over the default parameters
180
+ # @return [User, nil] the user or nil if the user was not found, whether the API answers 200 with no data or a 404
181
+ # that reports the user as not found
182
+ # @raise [X::NotFound] if the API answers any other 404, such as one from a client pointed at the wrong host or
183
+ # API version
184
+ # @raise [ArgumentError] if the value is not a username
185
+ # @yieldparam problem [Problem] each problem the API reported, such as a user that was not found
186
+ # @example Look up a user by username
187
+ # X::User.find_by_username("sferik", client: client)
188
+ def find_by_username(username, client:, **params, &)
189
+ locate("users/by/username/#{Utils.username!(username)}", client:, **params, &) #: User?
190
+ end
191
+
192
+ # Look up a user by username, which must exist
193
+ #
194
+ # @api public
195
+ # @param username [String] the username, with or without a leading at sign
196
+ # @param client [Object] the client used to make the request
197
+ # @param params [Hash] query parameters merged over the default parameters
198
+ # @return [User] the user
199
+ # @raise [ArgumentError] if the value is not a username
200
+ # @raise [MissingResource] if the user was not found, whether the API answers 200 with no data or a 404 that
201
+ # reports the user as not found
202
+ # @raise [X::NotFound] if the API answers any other 404, such as one from a client pointed at the wrong host or
203
+ # API version
204
+ # @example Look up a user by username
205
+ # X::User.find_by_username!("sferik", client: client)
206
+ def find_by_username!(username, client:, **params)
207
+ locate!("users/by/username/#{Utils.username!(username)}", "@#{Utils.username(username)}", client:, **params) #: User
208
+ end
209
+
210
+ # Look up the authenticated user
211
+ #
212
+ # Its path names no user, so a 404 to it is no user that is missing, and raises X::NotFound as any request does.
213
+ #
214
+ # @api public
215
+ # @param client [Object] the client used to make the request
216
+ # @param params [Hash] query parameters merged over the default parameters
217
+ # @return [User, nil] the authenticated user
218
+ # @yieldparam problem [Problem] each problem the API reported
219
+ # @example Look up the authenticated user
220
+ # X::User.current(client: client)
221
+ def current(client:, **params, &)
222
+ lookup("users/me", client:, **params, &) #: User?
223
+ end
224
+
225
+ # Look up the authenticated user, who must be found
226
+ #
227
+ # @api public
228
+ # @param client [Object] the client used to make the request
229
+ # @param params [Hash] query parameters merged over the default parameters
230
+ # @return [User] the authenticated user
231
+ # @raise [MissingResource] if the API returns no user
232
+ # @example Look up the authenticated user
233
+ # X::User.current!(client: client)
234
+ def current!(client:, **params)
235
+ problems = [] #: Array[Problem]
236
+ current(client:, **params) { |problem| problems << problem } || raise(MissingResource.new("users/me returned no user", problems:))
237
+ end
238
+
239
+ private
240
+
241
+ # Order users as they were asked for, one for each value that names one
242
+ # @api private
243
+ # @param users [Array<User>] the users found
244
+ # @param ids_or_usernames [Array<Integer, User, String>] the identifiers, users, and usernames asked for
245
+ # @return [Array<User>] the user each value names, in the order of the values, frozen
246
+ # @raise [InvalidAttribute] if the response holds a username that is not a String
247
+ def in_order(users, ids_or_usernames)
248
+ by_key = users.to_h { |user| [key_of(user), user] }.merge(users.filter_map { |user| username_key(user) }.to_h)
249
+ ids_or_usernames.filter_map { |value| by_key[key_of(value)] }.freeze
250
+ end
251
+
252
+ # The key that matches a user found to a username it was asked for by
253
+ #
254
+ # It is nil for a user without one. A username is a String, so it is keyed as one, after an at sign, and never
255
+ # matches an identifier.
256
+ #
257
+ # @api private
258
+ # @param user [User] the user found
259
+ # @return [Array(String, User), nil] the key of the username and the user, or nil if the user holds no username
260
+ # @raise [InvalidAttribute] if the response holds a username that is not a String
261
+ def username_key(user)
262
+ username = user.username
263
+ ["@#{Utils.read("#{self}#username", username) { normalize(String.try_convert(username) || raise(ArgumentError)) }}", user] unless username.nil?
264
+ end
265
+
266
+ # The key that matches a user to the identifier or username it was asked for by
267
+ # @api private
268
+ # @param value [Integer, User, String] an identifier or user, or a username
269
+ # @return [String] the identifier, or the username in lowercase after an at sign
270
+ def key_of(value)
271
+ Utils.id?(value) ? Utils.id_of(value, self) : "@#{normalize(value)}"
272
+ end
273
+
274
+ # A username without an at sign, in lowercase, since case does not matter
275
+ # @api private
276
+ # @param username [String] the username, with or without a leading at sign
277
+ # @return [String] the normalized username
278
+ def normalize(username) = Utils.username(username).downcase
279
+ end
280
+ private_constant :UserFinders
281
+ end
282
+ end