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,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../post"
4
+ require_relative "../relation_writes"
5
+ require_relative "../utils"
6
+
7
+ module X
8
+ module Resources
9
+ module Actions
10
+ # Like, repost, and bookmark posts as the authenticated user
11
+ #
12
+ # Internal to x-resources: X::Resources::API includes it, and its methods are public API of the client that
13
+ # includes API, but the module is only how they are grouped, and some of them need the methods of another,
14
+ # so include API rather than this module alone.
15
+ #
16
+ # @api semipublic
17
+ module Engagement
18
+ # Like a post as the authenticated user
19
+ #
20
+ # @api public
21
+ # @param post [Post, String, Integer] the post or its identifier
22
+ # @return [Boolean] true if the authenticated user now likes the post
23
+ # @example Like a post
24
+ # client.like("1234567890")
25
+ def like(post)
26
+ RelationWrites.relate(self, current_user_id, "likes", {"tweet_id" => Utils.id_of(post, Post)}, "liked")
27
+ end
28
+
29
+ # Unlike a post as the authenticated user
30
+ #
31
+ # @api public
32
+ # @param post [Post, String, Integer] the post or its identifier
33
+ # @return [Boolean] true if the authenticated user no longer likes the post
34
+ # @example Unlike a post
35
+ # client.unlike("1234567890")
36
+ def unlike(post)
37
+ RelationWrites.unrelate(self, current_user_id, "likes", Utils.id_of(post, Post), "liked")
38
+ end
39
+
40
+ # Repost a post as the authenticated user
41
+ #
42
+ # @api public
43
+ # @param post [Post, String, Integer] the post or its identifier
44
+ # @return [Boolean] true if the authenticated user has reposted the post
45
+ # @example Repost a post
46
+ # client.repost("1234567890")
47
+ def repost(post)
48
+ RelationWrites.relate(self, current_user_id, "retweets", {"tweet_id" => Utils.id_of(post, Post)}, "retweeted")
49
+ end
50
+
51
+ # Undo a repost as the authenticated user
52
+ #
53
+ # @api public
54
+ # @param post [Post, String, Integer] the post or its identifier
55
+ # @return [Boolean] true if the authenticated user no longer reposts the post
56
+ # @example Undo a repost
57
+ # client.unrepost("1234567890")
58
+ def unrepost(post)
59
+ RelationWrites.unrelate(self, current_user_id, "retweets", Utils.id_of(post, Post), "retweeted")
60
+ end
61
+
62
+ # Bookmark a post as the authenticated user
63
+ #
64
+ # Bookmarking a post takes only OAuth 2.0 user context, which the object layer cannot route around, so a
65
+ # client that signs with OAuth 1.0a is refused.
66
+ #
67
+ # @api public
68
+ # @param post [Post, String, Integer] the post or its identifier
69
+ # @return [Boolean] true if the authenticated user has bookmarked the post
70
+ # @example Bookmark a post
71
+ # client.bookmark("1234567890")
72
+ def bookmark(post)
73
+ RelationWrites.relate(self, current_user_id, "bookmarks", {"tweet_id" => Utils.id_of(post, Post)}, "bookmarked")
74
+ end
75
+
76
+ # Remove a bookmark as the authenticated user
77
+ #
78
+ # Removing a bookmark takes only OAuth 2.0 user context, which the object layer cannot route around, so a
79
+ # client that signs with OAuth 1.0a is refused.
80
+ #
81
+ # @api public
82
+ # @param post [Post, String, Integer] the post or its identifier
83
+ # @return [Boolean] true if the authenticated user no longer has the post bookmarked
84
+ # @example Remove a bookmark
85
+ # client.unbookmark("1234567890")
86
+ def unbookmark(post)
87
+ RelationWrites.unrelate(self, current_user_id, "bookmarks", Utils.id_of(post, Post), "bookmarked")
88
+ end
89
+
90
+ alias_method :retweet, :repost
91
+ alias_method :unretweet, :unrepost
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../list"
4
+ require_relative "../relation_writes"
5
+ require_relative "../user"
6
+ require_relative "../utils"
7
+
8
+ module X
9
+ module Resources
10
+ module Actions
11
+ # Create, update, delete, follow, and pin lists as the authenticated user
12
+ #
13
+ # Internal to x-resources: X::Resources::API includes it, and its methods are public API of the client that
14
+ # includes API, but the module is only how they are grouped, and some of them need the methods of another,
15
+ # so include API rather than this module alone.
16
+ #
17
+ # @api semipublic
18
+ module Lists
19
+ # Create a list owned by the authenticated user
20
+ #
21
+ # @api public
22
+ # @param name [String] the name of the list
23
+ # @param params [Hash] additional request body fields: description and private
24
+ # @return [List] the created list, holding only its identifier and name
25
+ # @raise [MissingResource] if the API answers without the list
26
+ # @example Create a private list
27
+ # client.create_list("Rubyists", private: true)
28
+ def create_list(name, **params)
29
+ List.create(name, client: self, **params)
30
+ end
31
+
32
+ # Update the name, description, or privacy of a list as the authenticated user
33
+ #
34
+ # @api public
35
+ # @param list [List, String, Integer] the list or its identifier
36
+ # @param params [Hash] the request body fields to change: name, description, and private
37
+ # @return [Boolean] true if the list was updated
38
+ # @raise [ArgumentError] if no field is given to change, before any request
39
+ # @example Make a list private
40
+ # client.update_list("1234567890", private: true)
41
+ def update_list(list, **params)
42
+ List.update(list, client: self, **params)
43
+ end
44
+
45
+ # Delete a list as the authenticated user
46
+ #
47
+ # @api public
48
+ # @param list [List, String, Integer] the list or its identifier
49
+ # @return [Boolean] true if the list was deleted
50
+ # @example Delete a list
51
+ # client.delete_list("1234567890")
52
+ def delete_list(list)
53
+ List.delete(list, client: self)
54
+ end
55
+
56
+ # Add a member to a list as the authenticated user
57
+ #
58
+ # @api public
59
+ # @param list [List, String, Integer] the list or its identifier
60
+ # @param user [User, String, Integer] the user or their identifier
61
+ # @return [Boolean] true if the user is now a member
62
+ # @example Add a member to a list
63
+ # client.add_list_member("1234567890", user)
64
+ def add_list_member(list, user)
65
+ List.from_id(list, client: self).add_member(user)
66
+ end
67
+
68
+ # Remove a member from a list as the authenticated user
69
+ #
70
+ # @api public
71
+ # @param list [List, String, Integer] the list or its identifier
72
+ # @param user [User, String, Integer] the user or their identifier
73
+ # @return [Boolean] true if the user is no longer a member
74
+ # @example Remove a member from a list
75
+ # client.remove_list_member("1234567890", user)
76
+ def remove_list_member(list, user)
77
+ List.from_id(list, client: self).remove_member(user)
78
+ end
79
+
80
+ # Follow a list as the authenticated user
81
+ #
82
+ # @api public
83
+ # @param list [List, String, Integer] the list or its identifier
84
+ # @return [Boolean] true if the authenticated user now follows the list
85
+ # @example Follow a list
86
+ # client.follow_list("1234567890")
87
+ def follow_list(list)
88
+ RelationWrites.relate(self, current_user_id, "followed_lists", {"list_id" => Utils.id_of(list, List)}, "following")
89
+ end
90
+
91
+ # Unfollow a list as the authenticated user
92
+ #
93
+ # @api public
94
+ # @param list [List, String, Integer] the list or its identifier
95
+ # @return [Boolean] true if the authenticated user no longer follows the list
96
+ # @example Unfollow a list
97
+ # client.unfollow_list("1234567890")
98
+ def unfollow_list(list)
99
+ RelationWrites.unrelate(self, current_user_id, "followed_lists", Utils.id_of(list, List), "following")
100
+ end
101
+
102
+ # Pin a list as the authenticated user
103
+ #
104
+ # @api public
105
+ # @param list [List, String, Integer] the list or its identifier
106
+ # @return [Boolean] true if the authenticated user has pinned the list
107
+ # @example Pin a list
108
+ # client.pin_list("1234567890")
109
+ def pin_list(list)
110
+ RelationWrites.relate(self, current_user_id, "pinned_lists", {"list_id" => Utils.id_of(list, List)}, "pinned")
111
+ end
112
+
113
+ # Unpin a list as the authenticated user
114
+ #
115
+ # @api public
116
+ # @param list [List, String, Integer] the list or its identifier
117
+ # @return [Boolean] true if the authenticated user no longer has the list pinned
118
+ # @example Unpin a list
119
+ # client.unpin_list("1234567890")
120
+ def unpin_list(list)
121
+ RelationWrites.unrelate(self, current_user_id, "pinned_lists", Utils.id_of(list, List), "pinned")
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../post"
4
+
5
+ module X
6
+ module Resources
7
+ module Actions
8
+ # Create and delete posts as the authenticated user
9
+ #
10
+ # Internal to x-resources: X::Resources::API includes it, and its methods are public API of the client that
11
+ # includes API, but the module is only how they are grouped, and some of them need the methods of another,
12
+ # so include API rather than this module alone.
13
+ #
14
+ # @api semipublic
15
+ module Posts
16
+ # Create a post as the authenticated user
17
+ #
18
+ # The API bills each post created, and bills a post whose text holds a URL more than ten times as much.
19
+ # A post needs no text when it has something else to show, such as media.
20
+ #
21
+ # @api public
22
+ # @param text [String, nil] the text of the post, or nil for a post without text, such as one of media alone
23
+ # @param params [Hash] additional request body fields, such as reply_to, quote, media_ids, or poll
24
+ # @return [Post] the created post, holding only its identifier and text
25
+ # @raise [ArgumentError] if the post has neither text nor any other field
26
+ # @raise [MissingResource] if the API answers without the post
27
+ # @example Create a post
28
+ # client.create_post("Hello, World!")
29
+ # @example Post an image without text
30
+ # client.create_post(media_ids: [media])
31
+ # @example Reply to a post with an image
32
+ # client.create_post("Hello!", reply_to: post, media_ids: [media["id"]])
33
+ # @example Quote a post
34
+ # client.create_post("Worth reading", quote: post)
35
+ def create_post(text = nil, **params) # steep:ignore DifferentMethodParameterKind
36
+ Post.create(text, client: self, **params)
37
+ end
38
+
39
+ # Delete a post as the authenticated user
40
+ #
41
+ # @api public
42
+ # @param post [Post, String, Integer] the post or its identifier
43
+ # @return [Boolean] true if the post was deleted
44
+ # @example Delete a post
45
+ # client.delete_post("1234567890")
46
+ def delete_post(post)
47
+ Post.delete(post, client: self)
48
+ end
49
+
50
+ # Hide a reply to a post of the authenticated user
51
+ #
52
+ # @api public
53
+ # @param post [Post, String, Integer] the reply or its identifier
54
+ # @return [Boolean] true if the reply is now hidden
55
+ # @example Hide a reply
56
+ # client.hide_reply("1234567890")
57
+ def hide_reply(post)
58
+ Post.hide_reply(post, client: self)
59
+ end
60
+
61
+ # Show a reply to a post of the authenticated user after hiding it
62
+ #
63
+ # @api public
64
+ # @param post [Post, String, Integer] the reply or its identifier
65
+ # @return [Boolean] true if the reply is no longer hidden
66
+ # @example Show a hidden reply
67
+ # client.unhide_reply("1234567890")
68
+ def unhide_reply(post)
69
+ Post.unhide_reply(post, client: self)
70
+ end
71
+
72
+ alias_method :create_tweet, :create_post
73
+ alias_method :delete_tweet, :delete_post
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../relation_writes"
4
+ require_relative "../user"
5
+ require_relative "../utils"
6
+
7
+ module X
8
+ module Resources
9
+ module Actions
10
+ # Follow, block, and mute users as the authenticated user
11
+ #
12
+ # Internal to x-resources: X::Resources::API includes it, and its methods are public API of the client that
13
+ # includes API, but the module is only how they are grouped, and some of them need the methods of another,
14
+ # so include API rather than this module alone.
15
+ #
16
+ # @api semipublic
17
+ module Relationships
18
+ # Follow a user as the authenticated user
19
+ #
20
+ # A protected user must accept a request to follow them first, so for a protected user true means the follow
21
+ # was requested, not that the authenticated user follows them: until they accept it, the follows? of the
22
+ # authenticated user, as in client.current_user!.follows?(user), answers false.
23
+ #
24
+ # @api public
25
+ # @param user [User, String, Integer] the user or their identifier
26
+ # @return [Boolean] true if the authenticated user now follows the user, or, for a protected user, has requested
27
+ # to follow them
28
+ # @example Follow a user
29
+ # client.follow("7505382")
30
+ def follow(user)
31
+ RelationWrites.relate(self, current_user_id, "following", {"target_user_id" => Utils.id_of(user, User)}, "following", "pending_follow")
32
+ end
33
+
34
+ # Unfollow a user as the authenticated user
35
+ #
36
+ # @api public
37
+ # @param user [User, String, Integer] the user or their identifier
38
+ # @return [Boolean] true if the authenticated user no longer follows the user
39
+ # @example Unfollow a user
40
+ # client.unfollow("7505382")
41
+ def unfollow(user)
42
+ RelationWrites.unrelate(self, current_user_id, "following", Utils.id_of(user, User), "following")
43
+ end
44
+
45
+ # Block a user as the authenticated user
46
+ #
47
+ # @api public
48
+ # @param user [User, String, Integer] the user or their identifier
49
+ # @return [Boolean] true if the authenticated user now blocks the user
50
+ # @example Block a user
51
+ # client.block("7505382")
52
+ def block(user)
53
+ RelationWrites.relate(self, current_user_id, "blocking", {"target_user_id" => Utils.id_of(user, User)}, "blocking")
54
+ end
55
+
56
+ # Unblock a user as the authenticated user
57
+ #
58
+ # @api public
59
+ # @param user [User, String, Integer] the user or their identifier
60
+ # @return [Boolean] true if the authenticated user no longer blocks the user
61
+ # @example Unblock a user
62
+ # client.unblock("7505382")
63
+ def unblock(user)
64
+ RelationWrites.unrelate(self, current_user_id, "blocking", Utils.id_of(user, User), "blocking")
65
+ end
66
+
67
+ # Mute a user as the authenticated user
68
+ #
69
+ # @api public
70
+ # @param user [User, String, Integer] the user or their identifier
71
+ # @return [Boolean] true if the authenticated user now mutes the user
72
+ # @example Mute a user
73
+ # client.mute("7505382")
74
+ def mute(user)
75
+ RelationWrites.relate(self, current_user_id, "muting", {"target_user_id" => Utils.id_of(user, User)}, "muting")
76
+ end
77
+
78
+ # Unmute a user as the authenticated user
79
+ #
80
+ # @api public
81
+ # @param user [User, String, Integer] the user or their identifier
82
+ # @return [Boolean] true if the authenticated user no longer mutes the user
83
+ # @example Unmute a user
84
+ # client.unmute("7505382")
85
+ def unmute(user)
86
+ RelationWrites.unrelate(self, current_user_id, "muting", Utils.id_of(user, User), "muting")
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "actions/direct_messages"
4
+ require_relative "actions/engagement"
5
+ require_relative "actions/lists"
6
+ require_relative "actions/posts"
7
+ require_relative "actions/relationships"
8
+
9
+ module X
10
+ module Resources
11
+ # Actions taken as the authenticated user, the modules of which X::Resources::API includes
12
+ #
13
+ # Internal to x-resources: a namespace of the modules API includes into a client, and not itself included, so that
14
+ # the modules it holds are not constants of the client, where the name of one, such as Media, would shadow a
15
+ # constant of the same name in a class that inherits from the client. Include API rather than any of them.
16
+ #
17
+ # @api private
18
+ module Actions
19
+ end
20
+ private_constant :Actions
21
+ end
22
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "actions"
4
+ require_relative "lookups"
5
+
6
+ module X
7
+ module Resources
8
+ # The resource methods mixed into a client that responds to get, post, put, and delete
9
+ #
10
+ # The x gem includes it into X::Client. With x-core and x-resources alone, or with a client of your own,
11
+ # include it yourself: X::Client.include(X::Resources::API).
12
+ #
13
+ # It is the one module of the object layer to include. The modules it includes, those of Lookups and Actions,
14
+ # are internal: their methods are public API of the client that includes API, but how they are grouped can change
15
+ # within 1.x, and some need the methods of another, such as current_user_id. So are the modules the resource
16
+ # classes extend and include, such as Finders and Relationships: the methods they give a resource class are public
17
+ # API, the modules are not.
18
+ #
19
+ # Neither API nor a module it includes holds a constant, as a constant of a module is a constant of each class
20
+ # that includes it: a class that inherits from the client and names Media, or Users, reads the constant the name
21
+ # reads anywhere else, not a module of the object layer.
22
+ #
23
+ # @api public
24
+ module API
25
+ include Lookups::Users
26
+ include Lookups::Posts
27
+ include Lookups::Lists
28
+ include Lookups::Media
29
+ include Lookups::Spaces
30
+ include Lookups::Communities
31
+ include Lookups::DirectMessages
32
+ include Lookups::Trends
33
+ include Actions::Posts
34
+ include Actions::Lists
35
+ include Actions::DirectMessages
36
+ include Actions::Relationships
37
+ include Actions::Engagement
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "shape"
4
+ require_relative "utils"
5
+
6
+ module X
7
+ module Resources
8
+ # Class-level macros for declaring resource attributes and references
9
+ # @api private
10
+ module Attributes
11
+ # The value of a list the response omitted, which reads as empty, as a list of references does, rather than nil
12
+ EMPTY_LIST = [] #: Array[untyped]
13
+ EMPTY_LIST.freeze
14
+ private_constant :EMPTY_LIST
15
+ # The values a flag is read from, which the API gives as true or false, and a response that omits it as nil
16
+ FLAGS = [true, false, nil].freeze
17
+ private_constant :FLAGS
18
+ # Converters keyed by attribute type
19
+ CONVERTERS = {
20
+ raw: ->(value) { value },
21
+ media_key: ->(value) { value },
22
+ conversation_id: ->(value) { Shape.conversation_id(value) },
23
+ boolean: ->(value) { FLAGS.include?(value) ? value : raise(ArgumentError, "#{value.inspect} is not true or false") },
24
+ time: ->(value) { Utils.time(value) },
25
+ integer: ->(value) { Shape.integer(value) },
26
+ integers: ->(value) { (Shape.list(value) || EMPTY_LIST).map { |id| Shape.integer(id) }.freeze },
27
+ list: ->(value) { Shape.list(value) || EMPTY_LIST },
28
+ object: ->(value) { Shape.object(value) },
29
+ objects: ->(value) { Shape.object_list(value) },
30
+ range: ->(value) { Shape.range(value) },
31
+ requested_list: ->(value) { Shape.list(value) }
32
+ }.freeze
33
+
34
+ # The names of the attributes declared on this class, which pattern matching reads
35
+ #
36
+ # @api private
37
+ # @return [Array<Symbol>] the attribute names
38
+ # @example Get the attributes of a user
39
+ # X::User.__send__(:attribute_names)
40
+ def attribute_names
41
+ parent = superclass
42
+ @attribute_names ||= parent.respond_to?(:attribute_names, true) ? parent.__send__(:attribute_names).dup : [:id]
43
+ end
44
+
45
+ # The other names some attributes are read by, which a pattern can ask for
46
+ #
47
+ # @api private
48
+ # @return [Array<Symbol>] the alias names
49
+ # @example Get the attribute aliases of a user
50
+ # X::User.__send__(:attribute_aliases) # => [:tweet_count, :pinned_tweet_id, :most_recent_tweet_id]
51
+ def attribute_aliases
52
+ parent = superclass
53
+ @attribute_aliases ||= parent.respond_to?(:attribute_aliases, true) ? parent.__send__(:attribute_aliases).dup : []
54
+ end
55
+
56
+ # The key paths of the identifiers of the resources this class refers to
57
+ #
58
+ # The problems of a resource are read by them. A key path holds an identifier, a list of them, or a list of objects that each hold one as id, as the
59
+ # referenced_posts of a post do.
60
+ #
61
+ # @api private
62
+ # @return [Array<Array<String>>] the key paths
63
+ # @example Get the key paths of the references of a list
64
+ # X::List.__send__(:reference_keys) # => [["owner_id"]]
65
+ def reference_keys
66
+ parent = superclass
67
+ @reference_keys ||= parent.respond_to?(:reference_keys, true) ? parent.__send__(:reference_keys).dup : []
68
+ end
69
+
70
+ # The identifiers of the resources that attributes of this class refer to
71
+ #
72
+ # A problem is read to tell which resource it is about, so a key path the attributes hold something other than
73
+ # an object along reads as no identifier, rather than raise as the reader of the reference does.
74
+ #
75
+ # @api private
76
+ # @param attrs [Hash{String => Object}] the attributes
77
+ # @return [Array<Object>] the identifiers, as the attributes hold them
78
+ # @example Get the identifiers a post refers to
79
+ # X::Post.__send__(:referenced_ids, {"id" => "1", "author_id" => "2"}) # => ["2"]
80
+ def referenced_ids(attrs) = reference_keys.flat_map { |path| ids_at(attrs, path) }
81
+
82
+ private :attribute_names, :attribute_aliases, :reference_keys, :referenced_ids
83
+
84
+ private
85
+
86
+ # Define another name for an attribute, which a pattern can ask for
87
+ #
88
+ # @api private
89
+ # @param name [Symbol] the alias name
90
+ # @param original [Symbol] the name of the attribute
91
+ # @return [void]
92
+ def attribute_alias(name, original)
93
+ attribute_aliases << name
94
+ alias_method name, original
95
+ end
96
+
97
+ # Define a reader for an attribute
98
+ #
99
+ # @api private
100
+ # @param name [Symbol] the reader name
101
+ # @param type [Symbol] the attribute type: raw, boolean, time, integer, integers, list, object, objects, range,
102
+ # conversation_id, or requested_list, of which integers, list, and objects read a list the response omitted as empty, and
103
+ # requested_list, the type of a field no lookup asks for unless it is requested, reads it as nil, since a
104
+ # response that omits it does not say the list is empty; object is the type of an object the API nests in a
105
+ # resource, and objects of a list of them
106
+ # @param key [Array<String>] the key path
107
+ # @param tweet_key [Array<String>, nil] the key path of the name the API gave the attribute before it named
108
+ # tweets posts, which it still gives it where it has not renamed it, such as in a stream, read when the
109
+ # response holds nothing at the key path
110
+ # @return [void]
111
+ # @raise [InvalidAttribute] from the reader, if the response holds a value the type cannot be read from
112
+ def attribute(name, type = :raw, key: [name.to_s], tweet_key: nil)
113
+ attribute_names << name
114
+ paths = key_paths(key, tweet_key)
115
+ converter = CONVERTERS.fetch(type)
116
+ define_method(name) do
117
+ # @type self: Resource
118
+ Utils.read("#{self.class}##{name}", Shape.dig_first("#{self.class}##{name}", attrs, paths)) { |value| converter.call(value) }
119
+ end
120
+ return unless type.eql?(:boolean)
121
+
122
+ define_method(:"#{name}?") do
123
+ # @type self: Resource
124
+ public_send(name).eql?(true)
125
+ end
126
+ end
127
+
128
+ # Define a reader that resolves a referenced resource
129
+ #
130
+ # @api private
131
+ # @param name [Symbol] the reader name
132
+ # @param klass_name [Symbol] the referenced resource class name under X
133
+ # @param key [Array<String>] the key path holding the identifier
134
+ # @param tweet_key [Array<String>, nil] the key path of the name the API gave it before it named tweets posts,
135
+ # read when the response holds nothing at the key path
136
+ # @return [void]
137
+ # @raise [InvalidAttribute] from the reader, if the response holds an identifier that is not one, or a key path
138
+ # that passes through something other than an object
139
+ def reference(name, klass_name, key:, tweet_key: nil)
140
+ paths = key_paths(key, tweet_key)
141
+ reference_keys.concat(paths)
142
+ define_method(name) do
143
+ # @type self: Resource
144
+ resolve(X.const_get(klass_name), Shape.dig_first("#{self.class}##{name}", attrs, paths))
145
+ end
146
+ end
147
+
148
+ # Define a reader that resolves a list of referenced resources
149
+ #
150
+ # @api private
151
+ # @param name [Symbol] the reader name
152
+ # @param klass_name [Symbol] the referenced resource class name under X
153
+ # @param key [Array<String>] the key path holding the identifiers
154
+ # @return [void]
155
+ # @raise [InvalidAttribute] from the reader, if the response holds something other than a list of identifiers
156
+ def references(name, klass_name, key:)
157
+ path = key_path(key)
158
+ reference_keys << path
159
+ define_method(name) do
160
+ # @type self: Resource
161
+ reader = "#{self.class}##{name}"
162
+ ids = Utils.read(reader, Shape.dig(reader, attrs, path)) { |value| Shape.list(value) } || EMPTY_LIST
163
+ ids.map { |id| resolve(X.const_get(klass_name), id) }.freeze
164
+ end
165
+ end
166
+
167
+ # Check that a key path is an array of keys
168
+ #
169
+ # @api private
170
+ # @param key [Object] the key path to check
171
+ # @return [Array<String>] the key path
172
+ # @raise [ArgumentError] if the key path is not an array
173
+ def key_path(key)
174
+ raise ArgumentError, "key must be an Array of keys, not #{key.inspect}" unless key.is_a?(Array)
175
+
176
+ key
177
+ end
178
+
179
+ # The key paths an attribute is read at, in the order they are tried
180
+ #
181
+ # @api private
182
+ # @param key [Object] the key path
183
+ # @param tweet_key [Object, nil] the key path of the name the API gave it before it named tweets posts, if any
184
+ # @return [Array<Array<String>>] the key paths
185
+ # @raise [ArgumentError] if a key path is not an array
186
+ def key_paths(key, tweet_key) = [key, tweet_key].compact.each { |path| key_path(path) }
187
+
188
+ # The identifiers at a key path
189
+ #
190
+ # A key path holds an identifier, a list of them, or a list of objects that each hold one as id.
191
+ #
192
+ # @api private
193
+ # @param attrs [Hash{String => Object}] the attributes
194
+ # @param path [Array<String>] the key path
195
+ # @return [Array<Object>] the identifiers, with nil for a path that holds none
196
+ def ids_at(attrs, path)
197
+ found = path.reduce(attrs) { |value, key| Hash.try_convert(value)&.[](key) }
198
+ (Array.try_convert(found) || [found]).map { |element| Hash.try_convert(element)&.[]("id") || element }
199
+ end
200
+ end
201
+ private_constant :Attributes
202
+ end
203
+ end