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,107 @@
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 rule of the filtered stream that a post the stream delivered matched: the identifier the API gave the rule, and
12
+ # the tag it is labelled with
13
+ #
14
+ # The stream names a rule a post matched by its identifier and its tag alone, not by the value it matches, so this is
15
+ # not an X::StreamRule of x-streams, which holds that value. It names the rule to X::StreamingClient#delete_rules
16
+ # by its identifier, as rule.id, or as rule.to_h, and it is found among the rules X::StreamingClient#rules reads by
17
+ # the identifier they share. x-resources depends on x-streams for none of this: a stream builds each post with
18
+ # X::Post.from_response, as a request does.
19
+ #
20
+ # It is frozen, compares equal to a rule of the same attributes, and matches a pattern of its identifier and tag,
21
+ # as in `rule in {tag: "ruby"}`. Its attributes are what the stream sent of the rule, as those of a resource are, so
22
+ # to_h, as_json, and to_json give them as the stream did, whatever the stream comes to send of a rule beside its
23
+ # identifier and tag.
24
+ #
25
+ # @api public
26
+ class ::X::MatchingRule
27
+ include Serialization
28
+ include ValueEquality
29
+ include ValueMarshalling
30
+
31
+ # The mixins of the class by their full names, which YARD needs to resolve them in a class opened under X
32
+ #
33
+ # @!parse
34
+ # include X::Resources::Serialization
35
+ # include X::Resources::ValueEquality
36
+ # include X::Resources::ValueMarshalling
37
+
38
+ # The attributes of the rule, as the stream sends them
39
+ #
40
+ # The identifier is a String, as the stream sends it, and a rule without a tag holds none.
41
+ #
42
+ # @api public
43
+ # @return [Hash{String => Object}] the attributes, frozen
44
+ # @example Get the attributes
45
+ # rule.attrs # => {"id" => "1165037377523306498", "tag" => "ruby"}
46
+ attr_reader :attrs
47
+
48
+ # Initialize a rule a post matched from the attributes the stream sent of it
49
+ #
50
+ # The identifier and tag are read as it is built, so that a rule that holds either as something else raises
51
+ # here, rather than from its readers.
52
+ #
53
+ # @api public
54
+ # @param attrs [Hash{String, Symbol => Object}] the attributes, which name the identifier as id, as an Integer or
55
+ # as the String the stream sends, and the tag as tag, unless the rule has none
56
+ # @return [MatchingRule] the frozen rule
57
+ # @raise [ArgumentError] if the attributes are not a Hash, the identifier is not one, or the tag is neither a
58
+ # String nor nil
59
+ # @example Build a rule a post matched
60
+ # X::MatchingRule.new({"id" => "1165037377523306498", "tag" => "ruby"})
61
+ def initialize(attrs)
62
+ @attrs = Utils.deep_freeze(Utils.attributes!(attrs))
63
+ id
64
+ raise ArgumentError, "tag must be a String, not #{tag.inspect}" unless tag.nil? || tag.is_a?(String)
65
+
66
+ freeze
67
+ end
68
+
69
+ # The identifier the API gave the rule
70
+ #
71
+ # The API sends it as a String, and it is read as an Integer, as the identifier of a stream rule is, and as
72
+ # strictly as any identifier is read: a String of digits alone, with no sign, underscore, or whitespace.
73
+ #
74
+ # @api public
75
+ # @return [Integer] the identifier
76
+ # @raise [ArgumentError] if the attributes hold no identifier, or one that is neither an Integer that is not
77
+ # negative nor a String of digits
78
+ # @example Get the identifier
79
+ # rule.id # => 1165037377523306498
80
+ def id = Shape.integer(attrs["id"]) || raise(ArgumentError, "a matching rule needs an id")
81
+
82
+ # The tag the rule is labelled with
83
+ # @api public
84
+ # @return [String, nil] the tag, or nil for a rule without one
85
+ # @example Get the tag
86
+ # rule.tag # => "ruby"
87
+ def tag = attrs["tag"]
88
+
89
+ # The identifier and tag of the rule, which a pattern matches against
90
+ #
91
+ # @api public
92
+ # @param keys [Array<Symbol>, nil] the keys the pattern asks for, or nil for both
93
+ # @return [Hash{Symbol => Integer, String, nil}] the identifier and tag the pattern asks for
94
+ # @example Keep the posts that matched the rule tagged ruby
95
+ # post.matching_rules.any? { |rule| rule in {tag: "ruby"} }
96
+ def deconstruct_keys(keys) = Utils.deconstruct(self, keys, %i[id tag])
97
+
98
+ # Summarize the rule for the console
99
+ #
100
+ # @api public
101
+ # @return [String] the class name, identifier, and tag
102
+ # @example Inspect a rule
103
+ # rule.inspect # => #<X::MatchingRule id=1165037377523306498 tag="ruby">
104
+ def inspect = "#<#{self.class} id=#{id} tag=#{tag.inspect}>"
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "batch_finders"
4
+ require_relative "media_ids"
5
+ require_relative "resource"
6
+
7
+ module X
8
+ module Resources
9
+ # A photo, video, or animated GIF attached to a post, which its media key identifies
10
+ # @api public
11
+ class ::X::Media < Resource
12
+ extend BatchFinders
13
+
14
+ # The mixins of the class by their full names, which YARD needs to resolve them in a class opened under X
15
+ #
16
+ # @!parse
17
+ # extend X::Resources::BatchFinders
18
+
19
+ # Every public media field
20
+ #
21
+ # A minor release may add to it the fields the API adds, so that a lookup asks for them too; see
22
+ # {Resource#hydrated?} for what that means for a resource looked up with a list of fields of its own.
23
+ FIELDS = %w[alt_text duration_ms height media_key preview_image_url public_metrics type url variants width].freeze
24
+
25
+ # The attribute holding the identifier
26
+ #
27
+ # @api private
28
+ # @return [String] the identifier key
29
+ # @example Get the identifier key
30
+ # X::Media.__send__(:id_key) # => "media_key"
31
+ def self.id_key
32
+ "media_key"
33
+ end
34
+
35
+ # The lookup endpoint
36
+ #
37
+ # @api private
38
+ # @return [String] the endpoint
39
+ # @example Get the lookup endpoint
40
+ # X::Media.__send__(:endpoint) # => "media"
41
+ def self.endpoint
42
+ "media"
43
+ end
44
+
45
+ # The query parameter a batch lookup takes the media keys in
46
+ #
47
+ # @api private
48
+ # @return [Symbol] the parameter name
49
+ # @example Get the parameter of a batch lookup
50
+ # X::Media.__send__(:batch_key) # => :media_keys
51
+ def self.batch_key = :media_keys
52
+
53
+ # The name of the fields parameter
54
+ #
55
+ # @api private
56
+ # @return [String] the fields parameter
57
+ # @example Get the fields parameter
58
+ # X::Media.__send__(:fields_key) # => "media.fields"
59
+ def self.fields_key = "media.fields"
60
+
61
+ # The default query parameters requesting every public field
62
+ #
63
+ # @api public
64
+ # @return [Hash{String => Array<String>}] the default query parameters
65
+ # @example Get the default fields
66
+ # X::Media.default_params["media.fields"]
67
+ def self.default_params
68
+ {"media.fields" => FIELDS}
69
+ end
70
+
71
+ # The type of the identifier, a media key
72
+ #
73
+ # Media is looked up by its media key, such as "3_1880028106020515840", and not by the numeric identifier an
74
+ # upload returns beside it, which the lookup endpoint does not find, so a number raises rather than look up
75
+ # nothing.
76
+ #
77
+ # @api private
78
+ # @return [Symbol] media_key
79
+ # @example Get the identifier type
80
+ # X::Media.__send__(:id_type) # => :media_key
81
+ def self.id_type = :media_key
82
+
83
+ # The media key of what an upload returned, or of a value that is one already
84
+ #
85
+ # What an upload returns holds both a media key and a numeric identifier, and the lookup endpoint takes the
86
+ # media key, so this reads that rather than the identifier a resource is usually found by.
87
+ #
88
+ # @api private
89
+ # @param media [#media_key, String] the media, or its media key
90
+ # @return [Object] the media key
91
+ # @example Get the media key of an upload
92
+ # X::Media.__send__(:key_of, uploaded) # => "3_1880028106020515840"
93
+ def self.key_of(media) = media.respond_to?(:media_key) ? media.media_key : media
94
+
95
+ # Look up media by media key
96
+ #
97
+ # @api public
98
+ # @param media [#media_key, String, Media] the media key, what an upload returned, or media
99
+ # @param client [Object] the client used to make the request
100
+ # @param params [Hash] query parameters merged over the default parameters
101
+ # @return [Media, nil] the media, or nil if it was not found
102
+ # @yieldparam problem [Problem] each problem the API reported
103
+ # @example Look up what an upload returned
104
+ # X::Media.find(uploaded, client: client)
105
+ def self.find(media, client:, **params) = super(key_of(media), client:, **params) #: Media?
106
+
107
+ # Look up media by media key, which must exist
108
+ #
109
+ # The error it raises names the media key looked up, whether a media key, what an upload returned, or media was
110
+ # given.
111
+ #
112
+ # @api public
113
+ # @param media [#media_key, String, Media] the media key, what an upload returned, or media
114
+ # @param client [Object] the client used to make the request
115
+ # @param params [Hash] query parameters merged over the default parameters
116
+ # @return [Media] the media
117
+ # @raise [MissingResource] if the media was not found
118
+ # @example Look up what an upload returned
119
+ # X::Media.find!(uploaded, client: client)
120
+ def self.find!(media, client:, **params) = super(key_of(media), client:, **params) #: Media
121
+
122
+ # Refer to media by media key without a request
123
+ #
124
+ # @api public
125
+ # @param media [#media_key, String, Media] the media key, what an upload returned, or media
126
+ # @param client [Object, nil] the client used to fetch the media
127
+ # @return [Media] a stub that hydrates to the full media
128
+ # @example Refer to what an upload returned
129
+ # X::Media.from_id(uploaded, client: client).hydrate.url
130
+ def self.from_id(media, client: nil) = super #: Media
131
+
132
+ # Build a stub of media that hydrates with the stubs of a batch
133
+ #
134
+ # @api private
135
+ # @param media [#media_key, String, Media] the media key, what an upload returned, or media
136
+ # @param client [Object, nil] the client used to fetch the media
137
+ # @param batch [Resources::Batch, nil] the batch the stub hydrates with
138
+ # @return [Media] a stub that hydrates to the full media
139
+ # @example Build the stub of a page of media
140
+ # X::Media.__send__(:from_id_in_batch, "3_1", client: client, batch: batch)
141
+ def self.from_id_in_batch(media, client:, batch: nil) = super(key_of(media), client:, batch:)
142
+
143
+ # Look up many media by media key, in parallel batches
144
+ #
145
+ # @api public
146
+ # @param media [Array<#media_key, String, Media>] the media keys, what the uploads returned, or media
147
+ # @param client [Object] the client used to make the requests
148
+ # @param params [Hash] query parameters merged over the default parameters, and the concurrency of the batches
149
+ # @return [Array<Media>] the media that was found
150
+ # @yieldparam problem [Problem] each problem the API reported
151
+ # @example Look up what the uploads returned
152
+ # X::Media.find_all(uploads, client: client)
153
+ def self.find_all(media, client:, concurrency: BatchFinders::DEFAULT_CONCURRENCY, **params) = super(media.map { |value| key_of(value) }, client:, concurrency:, **params) #: Array[Media]
154
+
155
+ # The key under which media appear in the includes of a response
156
+ #
157
+ # @api private
158
+ # @return [String] the includes key
159
+ # @example Get the includes key
160
+ # X::Media.__send__(:includes_key) # => "media"
161
+ def self.includes_key
162
+ "media"
163
+ end
164
+ private_class_method :id_key, :endpoint, :batch_key, :fields_key, :id_type, :key_of, :from_id_in_batch, :includes_key
165
+
166
+ # @!method id
167
+ # The identifier of the media, which is its media key
168
+ #
169
+ # Media is looked up by its media key, so that is what identifies it here, where the identifier of uploaded
170
+ # media, and the one a post or a direct message attaches media by, is the number media_id reads. Both X::Media
171
+ # and the uploaded media of x-uploads answer media_key and media_id alike.
172
+ #
173
+ # @api public
174
+ # @return [String] the media key
175
+ # @example Get the media key
176
+ # media.id # => "3_1880028106020515840"
177
+
178
+ # @!attribute [r] media_key
179
+ # The media key
180
+ # @api public
181
+ # @return [String] the media key
182
+ # @example Get the media key
183
+ # media.media_key
184
+ attribute :media_key
185
+
186
+ # The numeric identifier of the media, which its media key names
187
+ #
188
+ # A media key names the identifier after the number of the type of the media and an underscore. It is the
189
+ # identifier an upload returns, and the one a post or a direct message attaches media by.
190
+ #
191
+ # @api public
192
+ # @return [Integer] the media identifier
193
+ # @example Get the identifier the media key names
194
+ # X::Media.from_id("3_1880028106020515840").media_id # => 1880028106020515840
195
+ def media_id = Integer(MediaIds.media_key_id(self), 10)
196
+
197
+ # @!attribute [r] type
198
+ # The media type: photo, video, or animated_gif
199
+ # @api public
200
+ # @return [String, nil] the media type
201
+ # @example Get the type
202
+ # media.type
203
+ attribute :type
204
+
205
+ # @!attribute [r] url
206
+ # The URL of a photo
207
+ # @api public
208
+ # @return [String, nil] the URL
209
+ # @example Get the URL
210
+ # media.url
211
+ attribute :url
212
+
213
+ # @!attribute [r] preview_image_url
214
+ # The URL of a video or GIF preview image
215
+ # @api public
216
+ # @return [String, nil] the preview image URL
217
+ # @example Get the preview image URL
218
+ # media.preview_image_url
219
+ attribute :preview_image_url
220
+
221
+ # @!attribute [r] alt_text
222
+ # The alternative text
223
+ # @api public
224
+ # @return [String, nil] the alternative text
225
+ # @example Get the alternative text
226
+ # media.alt_text
227
+ attribute :alt_text
228
+
229
+ # @!attribute [r] duration_ms
230
+ # The duration of a video in milliseconds
231
+ # @api public
232
+ # @return [Integer, nil] the duration in milliseconds
233
+ # @example Get the duration
234
+ # media.duration_ms
235
+ attribute :duration_ms, :integer
236
+
237
+ # @!attribute [r] height
238
+ # The height in pixels
239
+ # @api public
240
+ # @return [Integer, nil] the height in pixels
241
+ # @example Get the height
242
+ # media.height
243
+ attribute :height, :integer
244
+
245
+ # @!attribute [r] width
246
+ # The width in pixels
247
+ # @api public
248
+ # @return [Integer, nil] the width in pixels
249
+ # @example Get the width
250
+ # media.width
251
+ attribute :width, :integer
252
+
253
+ # @!attribute [r] variants
254
+ # The video variants with their bit rates, content types, and URLs
255
+ # @api public
256
+ # @return [Array<Hash>] the variants, empty if there are none
257
+ # @example Get the variants
258
+ # media.variants
259
+ attribute :variants, :objects
260
+
261
+ # @!attribute [r] public_metrics
262
+ # The public metrics
263
+ # @api public
264
+ # @return [Hash, nil] the public metrics
265
+ # @example Get the public metrics
266
+ # media.public_metrics
267
+ attribute :public_metrics, :object
268
+
269
+ # @!attribute [r] view_count
270
+ # The number of views
271
+ # @api public
272
+ # @return [Integer, nil] the view count
273
+ # @example Get the view count
274
+ # media.view_count
275
+ attribute :view_count, :integer, key: %w[public_metrics view_count]
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X
4
+ module Resources
5
+ # Reads the media identifiers a new post or direct message attaches
6
+ #
7
+ # What an upload returns, media such as the media of another post, and a media identifier each name media, and
8
+ # this reads the identifier the API takes from any of them.
9
+ #
10
+ # @api private
11
+ module MediaIds
12
+ # The pattern of a media key, which names the media identifier after the number of its type and an underscore
13
+ MEDIA_KEY = /\A\d+_(\d+)\z/
14
+
15
+ # The pattern of a media identifier the API takes: one to nineteen digits
16
+ MEDIA_ID = /\A\d{1,19}\z/
17
+
18
+ # The message of the error raised for something that is neither media nor the identifier of media
19
+ NOT_MEDIA = "media is what an upload returned, media such as X::Media, a media key, or a media identifier, not %s"
20
+ private_constant :MEDIA_KEY, :MEDIA_ID, :NOT_MEDIA
21
+
22
+ extend self
23
+
24
+ # Extract a media identifier from an upload, from media, or from a raw value
25
+ #
26
+ # What an upload returns is read with fetch, which a Hash answers and so does the uploaded media of
27
+ # x-uploads, which this gem does not depend on. Media, such as the media of a post, is read from its media
28
+ # key, which names the identifier, and so is a String that is a media key, such as the id of X::Media.
29
+ #
30
+ # @api private
31
+ # @param value [#fetch, #media_key, String, Integer] what an upload returned, holding an id, media, a media key,
32
+ # or an identifier
33
+ # @return [String] the media identifier
34
+ # @raise [ArgumentError] if the value is none of them, such as a Hash without an "id", or names an identifier the
35
+ # API would refuse
36
+ def media_id_of(value)
37
+ id = case value
38
+ when String then value[MEDIA_KEY, 1] || value
39
+ when Integer then value.to_s
40
+ else value.respond_to?(:fetch) ? value.fetch("id") { raise ArgumentError, "#{format(NOT_MEDIA, value.inspect)}, which holds no \"id\"" }.to_s : media_key_id(value)
41
+ end
42
+ id.match?(MEDIA_ID) ? id : raise(ArgumentError, format(NOT_MEDIA, value.inspect))
43
+ end
44
+
45
+ # The media identifier a media key names
46
+ # @api private
47
+ # @param media [#media_key, Object] the media
48
+ # @return [String] the media identifier
49
+ # @raise [ArgumentError] if the media has no media key that names an identifier
50
+ def media_key_id(media)
51
+ key = media.media_key if media.respond_to?(:media_key)
52
+ key.to_s[MEDIA_KEY, 1] || raise(ArgumentError, format(NOT_MEDIA, media.inspect))
53
+ end
54
+
55
+ # The media identifiers of one upload or of several
56
+ #
57
+ # One upload needs no array around it, so a single value is read as a list of one, and nil, like an empty
58
+ # list, as none.
59
+ #
60
+ # @api private
61
+ # @param media_ids [Array, #fetch, Media, String, Integer, nil] what the uploads returned, media, or identifiers,
62
+ # one or many
63
+ # @return [Array<String>] the media identifiers, empty for nil
64
+ def media_ids_of(media_ids)
65
+ case media_ids
66
+ when nil then []
67
+ when Array then media_ids.map { |media| media_id_of(media) }
68
+ else [media_id_of(media_ids)]
69
+ end
70
+ end
71
+ end
72
+ private_constant :MediaIds
73
+ end
74
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module X
6
+ module Resources
7
+ # A thread-safe slot that holds one lazily computed value
8
+ # @api private
9
+ class Memo
10
+ # Marker for a slot that holds no value yet
11
+ UNSET = Object.new.freeze
12
+ private_constant :UNSET
13
+
14
+ # Initialize an empty memo
15
+ #
16
+ # @api private
17
+ # @return [Memo] a new memo
18
+ def initialize
19
+ @monitor = Monitor.new
20
+ @value = UNSET
21
+ end
22
+
23
+ # Fetch the value, computing and storing it on first access
24
+ #
25
+ # @api private
26
+ # @yield computes the value when none is stored
27
+ # @return [Object] the memoized value
28
+ def fetch
29
+ @monitor.synchronize do
30
+ @value = yield if @value.equal?(UNSET)
31
+ @value
32
+ end
33
+ end
34
+
35
+ # Check whether a value is stored, whether computed or stored
36
+ #
37
+ # It reads the slot without the lock, since a value, once stored, is only ever replaced by another.
38
+ #
39
+ # @api private
40
+ # @return [Boolean] true if the memo holds a value, which may be nil
41
+ def stored? = !@value.equal?(UNSET)
42
+
43
+ # Store a value, replacing any memoized value
44
+ #
45
+ # @api private
46
+ # @param value [Object] the value to store
47
+ # @return [Object] the stored value
48
+ def store(value)
49
+ @monitor.synchronize { @value = value }
50
+ end
51
+ end
52
+ private_constant :Memo
53
+ end
54
+ end