globalid 1.2.1 → 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 18495b4f7910329fd0a65dcfa294742d0cdbb129a4c7f6971c4f9c62e9ea890d
4
- data.tar.gz: 5b6a25aabd0d04d9a4a2d4bb4d5152d68a67758fad96deabc011a4a169f2a055
3
+ metadata.gz: d57eeec2571a0ec07f96c46e09a1f2513958e798069847b6001c08088ebcb69c
4
+ data.tar.gz: d6585f62af8ae463201fdc6994d9f816c71e793dffb00803561e53bde284711e
5
5
  SHA512:
6
- metadata.gz: fb525d60050154b885197a89aaf2b1f2f577d0574b83160a3c0cbc2899d67e8995cf23c2de852c30baaa1ef8cc594cad39187e3808b350195c95ead32359957c
7
- data.tar.gz: 5dda5c09cffcc6e60da06931f3be5ba144db99f6ed0b9d7dc085633e0bac642f1c9d9ac7ba686dd59a6b9be64bb275ee8e78b39fae6e372b69c8131709db6f0f
6
+ metadata.gz: 3fdc67028c552eb91fed9fc35031c5dec4215090260c21ce3d4d10b2db511be531162f3f93f7710ad15534e747a5610e0f9ecab5bed5c9fc5a45f30ee9936dd8
7
+ data.tar.gz: 2d07eeee13b8337cea053b4c67ba52dd5a9f05ff80e53b123d97364cf3dc0bf32654ff64c4a3f882fa69a44c960d2e11a2f7bb6a4edaf717331ebaf8ebe65528
data/README.md CHANGED
@@ -37,6 +37,23 @@ GlobalID::Locator.locate person_gid
37
37
  # => #<Person:0x007fae94bf6298 @id="1">
38
38
  ```
39
39
 
40
+ `locate` returns `nil` for a blank or unparseable Global ID, and lets the
41
+ backend's own exceptions bubble up when a record can't be found. Use `fetch`
42
+ when you want to tell apart a record that's gone for good from a transient
43
+ backend failure:
44
+
45
+ ```ruby
46
+ GlobalID::Locator.fetch person_gid
47
+ # => #<Person:0x007fae94bf6298 @id="1"> # found
48
+ # => raises GlobalID::Locator::RecordNotFound # the record no longer exists
49
+ # => raises GlobalID::Locator::RecordUnavailable # the backend failed; retry may succeed
50
+ ```
51
+
52
+ Both errors extend `GlobalID::Locator::Error`, so you can rescue either at
53
+ once. This is useful, for example, to discard a background job whose argument
54
+ points at a deleted record, without also discarding jobs that hit a temporary
55
+ database error.
56
+
40
57
  ### Signed Global IDs
41
58
 
42
59
  For added security GlobalIDs can also be signed to ensure that the data hasn't been tampered with.
@@ -57,7 +74,7 @@ GlobalID::Locator.locate_signed person_sgid
57
74
 
58
75
  **Expiration**
59
76
 
60
- Signed Global IDs can expire some time in the future. This is useful if there's a resource
77
+ Signed Global IDs can expire sometime in the future. This is useful if there's a resource
61
78
  people shouldn't have indefinite access to, like a share link.
62
79
 
63
80
  ```ruby
@@ -73,8 +90,8 @@ GlobalID::Locator.locate_signed(expiring_sgid.to_s, for: 'sharing')
73
90
  # => nil
74
91
  ```
75
92
 
76
- **In Rails, an auto-expiry of 1 month is set by default.** You can alter that deal
77
- in an initializer with:
93
+ **In Rails, an auto-expiry of 1 month is set by default.** You can alter that
94
+ default in an initializer with:
78
95
 
79
96
  ```ruby
80
97
  # config/initializers/global_id.rb
@@ -87,7 +104,7 @@ You can assign a default SGID lifetime like so:
87
104
  SignedGlobalID.expires_in = 1.month
88
105
  ```
89
106
 
90
- This way any generated SGID will use that relative expiry.
107
+ This way, any generated SGID will use that relative expiry.
91
108
 
92
109
  It's worth noting that _expiring SGIDs are not idempotent_ because they encode the current timestamp; repeated calls to `to_sgid` will produce different results. For example, in Rails
93
110
 
@@ -165,19 +182,19 @@ Note the order is maintained in the returned results.
165
182
 
166
183
  Either `GlobalID::Locator.locate` or `GlobalID::Locator.locate_many` supports a hash of options as second parameter. The supported options are:
167
184
 
168
- * :includes - A Symbol, Array, Hash or combination of them
169
- The same structure you would pass into a `includes` method of Active Record.
170
- See [Active Record eager loading associations](https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations)
185
+ * `:includes` - A Symbol, Array, Hash or combination of them.
186
+ The same structure you would pass into an `includes` method of Active Record.
187
+ See [Active Record eager loading associations](https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations).
171
188
  If present, `locate` or `locate_many` will eager load all the relationships specified here.
172
- Note: It only works if all the gids models have that relationships.
173
- * :only - A class, module or Array of classes and/or modules that are
189
+ Note: It only works if all the GIDs Models have those relationships.
190
+ * `:only` - A class, module, or Array of classes and/or modules that are
174
191
  allowed to be located. Passing one or more classes limits instances of returned
175
192
  classes to those classes or their subclasses. Passing one or more modules in limits
176
193
  instances of returned classes to those including that module. If no classes or
177
- modules match, +nil+ is returned.
178
- * :ignore_missing (Only for `locate_many`) - By default, `locate_many` will call `#find` on the model to locate the
194
+ modules match, `nil` is returned.
195
+ * `:ignore_missing` (Only for `locate_many`) - By default, `locate_many` will call `#find` on the model to locate the
179
196
  ids extracted from the GIDs. In Active Record (and other data stores following the same pattern),
180
- `#find` will raise an exception if a named ID can't be found. When you set this option to true,
197
+ `#find` will raise an exception if a named ID can't be found. When you set this option to `true`,
181
198
  we will use `#where(id: ids)` instead, which does not raise on missing records.
182
199
 
183
200
  ### Custom App Locator
@@ -199,17 +216,85 @@ end
199
216
  Using a class:
200
217
 
201
218
  ```ruby
202
- GlobalID::Locator.use :bar, BarLocator.new
203
219
  class BarLocator
204
220
  def locate(gid, options = {})
205
221
  @search_client.search name: gid.model_name, id: gid.model_id
206
222
  end
207
223
  end
224
+
225
+ GlobalID::Locator.use :bar, BarLocator.new
226
+ ```
227
+
228
+ It's recommended to inherit from `GlobalID::Locator::BaseLocator` (or `GlobalID::Locator::UnscopedLocator` for Active Record models) to get default implementations of `model_class` and `locate_many`:
229
+
230
+ ```ruby
231
+ class BarLocator < GlobalID::Locator::BaseLocator
232
+ def locate(gid, options = {})
233
+ @search_client.search name: gid.model_name, id: gid.model_id
234
+ end
235
+ end
236
+
237
+ GlobalID::Locator.use :bar, BarLocator.new
208
238
  ```
209
239
 
210
- After defining locators as above, URIs like "gid://foo/Person/1" and "gid://bar/Person/1" will now use the foo block locator and `BarLocator` respectively.
240
+ After defining locators as above, URIs like `gid://foo/Person/1` and `gid://bar/Person/1` will now use the foo block locator and `BarLocator` respectively.
211
241
  Other apps will still keep using the default locator.
212
242
 
243
+ #### Custom Model Class Derivation
244
+
245
+ By default, GlobalID derives the model class by calling `constantize` on the model name from the GID. Custom locators can override this behavior by implementing a `model_class` method. This is useful when the model name in the GID doesn't match the actual class name, or when you want to redirect to a different model.
246
+
247
+ Inherit from `BaseLocator` and override `model_class`:
248
+
249
+ ```ruby
250
+ class RemoteLocator < GlobalID::Locator::BaseLocator
251
+ def model_class(gid)
252
+ # Map remote model names to local models
253
+ case gid.model_name
254
+ when 'User'
255
+ RemoteUser
256
+ when 'Profile'
257
+ RemoteProfile
258
+ else
259
+ super # Fall back to default constantize behavior
260
+ end
261
+ end
262
+
263
+ def locate(gid, options = {})
264
+ # Use the mapped model class to find the record
265
+ model_class(gid).find_by(remote_id: gid.model_id)
266
+ end
267
+ end
268
+
269
+ GlobalID::Locator.use :remote, RemoteLocator.new
270
+ ```
271
+
272
+ This allows you to work with Global IDs that reference models that don't exist in your application, redirecting them to the appropriate local models.
273
+
274
+ **Note**: For backward compatibility, if a custom locator doesn't implement `model_class`, GlobalID will fall back to the default behavior (`constantize`) but will emit a deprecation warning. To avoid this, inherit from `GlobalID::Locator::BaseLocator` or `GlobalID::Locator::UnscopedLocator`.
275
+
276
+ ### Custom Default Locator
277
+
278
+ A custom default locator can be set for an app by calling `GlobalID::Locator.default_locator=` and providing a default locator to use for that app.
279
+
280
+ ```ruby
281
+ class MyCustomLocator < UnscopedLocator
282
+ def locate(gid, options = {})
283
+ ActiveRecord::Base.connected_to(role: :reading) do
284
+ super(gid, options)
285
+ end
286
+ end
287
+
288
+ def locate_many(gids, options = {})
289
+ ActiveRecord::Base.connected_to(role: :reading) do
290
+ super(gids, options)
291
+ end
292
+ end
293
+ end
294
+
295
+ GlobalID::Locator.default_locator = MyCustomLocator.new
296
+ ```
297
+
213
298
  ## Contributing to GlobalID
214
299
 
215
300
  GlobalID is work of many contributors. You're encouraged to submit pull requests, propose
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'active_support/core_ext/string/inflections' # For #model_class constantize
2
3
  require 'active_support/core_ext/array/access'
3
4
  require 'active_support/core_ext/object/try' # For #find
@@ -32,6 +33,10 @@ class GlobalID
32
33
  @app = URI::GID.validate_app(app)
33
34
  end
34
35
 
36
+ def default_locator(default_locator)
37
+ Locator.default_locator = default_locator
38
+ end
39
+
35
40
  private
36
41
  def parse_encoded_gid(gid, options)
37
42
  new(Base64.urlsafe_decode64(gid), options) rescue nil
@@ -51,8 +56,21 @@ class GlobalID
51
56
 
52
57
  def model_class
53
58
  @model_class ||= begin
54
- model = model_name.constantize
55
-
59
+ locator = Locator.locator_for(self)
60
+ model = begin
61
+ locator.model_class(self)
62
+ rescue NoMethodError
63
+ if locator.respond_to?(:model_class)
64
+ raise
65
+ else
66
+ GlobalID.deprecator.warn <<~MSG.squish
67
+ Your locator #{locator.class.name} does not implement the
68
+ `model_class` method. Please add a `model_class(gid)` method
69
+ to your locator or inherit from `GlobalID::Locator::BaseLocator`.
70
+ MSG
71
+ model_name.constantize
72
+ end
73
+ end
56
74
  if model <= GlobalID
57
75
  raise ArgumentError, "GlobalID and SignedGlobalID cannot be used as model_class."
58
76
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class GlobalID
2
3
  # Mix `GlobalID::Identification` into any model with a `#find(id)` class
3
4
  # method. Support is automatically included in Active Record.
@@ -31,8 +32,8 @@ class GlobalID
31
32
  #
32
33
  # model = Person.new id: 1
33
34
  # global_id = model.to_global_id
34
- # global_id.modal_class # => Person
35
- # global_id.modal_id # => "1"
35
+ # global_id.model_class # => Person
36
+ # global_id.model_id # => "1"
36
37
  # global_id.to_param # => "Z2lkOi8vYm9yZGZvbGlvL1BlcnNvbi8x"
37
38
  def to_global_id(options = {})
38
39
  GlobalID.create(self, options)
@@ -52,8 +53,8 @@ class GlobalID
52
53
  #
53
54
  # model = Person.new id: 1
54
55
  # signed_global_id = model.to_signed_global_id
55
- # signed_global_id.modal_class # => Person
56
- # signed_global_id.modal_id # => "1"
56
+ # signed_global_id.model_class # => Person
57
+ # signed_global_id.model_id # => "1"
57
58
  # signed_global_id.to_param # => "BAh7CEkiCGdpZAY6BkVUSSIiZ2..."
58
59
  #
59
60
  # ==== Expiration
@@ -1,10 +1,25 @@
1
+ # frozen_string_literal: true
1
2
  require 'active_support/core_ext/enumerable' # For Enumerable#index_by
2
3
 
3
4
  class GlobalID
4
5
  module Locator
5
6
  class InvalidModelIdError < StandardError; end
7
+ class Error < StandardError; end
8
+
9
+ # Raised by GlobalID::Locator.fetch when the GlobalID is valid but the
10
+ # record it references no longer exists. The record is gone for good, so
11
+ # retrying won't help.
12
+ class RecordNotFound < Error; end
13
+
14
+ # Raised by GlobalID::Locator.fetch when the record couldn't be located
15
+ # due to any other error in the backend, such as a database connection
16
+ # error. The record may still exist, so retrying may succeed.
17
+ class RecordUnavailable < Error; end
6
18
 
7
19
  class << self
20
+ # The default locator used when no app-specific locator is found.
21
+ attr_accessor :default_locator
22
+
8
23
  # Takes either a GlobalID or a string that can be turned into a GlobalID
9
24
  #
10
25
  # Options:
@@ -20,7 +35,7 @@ class GlobalID
20
35
  def locate(gid, options = {})
21
36
  gid = GlobalID.parse(gid)
22
37
 
23
- return unless gid && find_allowed?(gid.model_class, options[:only])
38
+ return unless gid && find_allowed?(gid, options[:only])
24
39
 
25
40
  locator = locator_for(gid)
26
41
 
@@ -32,6 +47,35 @@ class GlobalID
32
47
  end
33
48
  end
34
49
 
50
+ # Like .locate, but instead of returning +nil+ or leaking the backend's
51
+ # own exceptions when the record can't be returned, it raises one of two
52
+ # GlobalID-specific errors so callers can tell the cases apart:
53
+ #
54
+ # * GlobalID::Locator::RecordNotFound when the record no longer exists.
55
+ # Retrying won't help.
56
+ # * GlobalID::Locator::RecordUnavailable when the record couldn't be
57
+ # located due to any other failure in the backend, like a database
58
+ # connection error. The record may still exist, so retrying may succeed.
59
+ #
60
+ # The distinction is drawn without knowing the backend's exception
61
+ # classes: the record is looked up through a query that doesn't raise on
62
+ # missing records (like .locate_many's +:ignore_missing+), so an empty
63
+ # result means the record is gone, while an error from the query itself
64
+ # means the backend is unavailable.
65
+ #
66
+ # Returns +nil+ for a blank or unparseable GlobalID, or one disallowed by
67
+ # the +:only+ option, just like .locate, and accepts the same options.
68
+ #
69
+ # Note: custom locators registered with .use need to implement locate_many
70
+ # with support for the +:ignore_missing+ option for this method to work.
71
+ def fetch(gid, options = {})
72
+ gid = GlobalID.parse(gid)
73
+
74
+ return unless gid && find_allowed?(gid, options[:only])
75
+
76
+ fetch_record(gid, options.except(:only)) or raise RecordNotFound, "Couldn't find record for #{gid}"
77
+ end
78
+
35
79
  # Takes an array of GlobalIDs or strings that can be turned into a GlobalIDs.
36
80
  # All GlobalIDs must belong to the same app, as they will be located using
37
81
  # the same locator using its locate_many method.
@@ -132,17 +176,23 @@ class GlobalID
132
176
  @locators[normalize_app(app)] = locator || BlockLocator.new(locator_block)
133
177
  end
134
178
 
179
+ def locator_for(gid)
180
+ @locators.fetch(normalize_app(gid.app)) { default_locator }
181
+ end
182
+
135
183
  private
136
- def locator_for(gid)
137
- @locators.fetch(normalize_app(gid.app)) { DEFAULT_LOCATOR }
184
+ def find_allowed?(gid, only = nil)
185
+ only ? Array(only).any? { |c| gid.model_class <= c } : true
138
186
  end
139
187
 
140
- def find_allowed?(model_class, only = nil)
141
- only ? Array(only).any? { |c| model_class <= c } : true
188
+ def fetch_record(gid, options)
189
+ locator_for(gid).locate_many([gid], options.merge(ignore_missing: true)).first
190
+ rescue => error
191
+ raise RecordUnavailable, "Couldn't fetch record for #{gid}: #{error.message}"
142
192
  end
143
193
 
144
194
  def parse_allowed(gids, only = nil)
145
- gids.collect { |gid| GlobalID.parse(gid) }.compact.select { |gid| find_allowed?(gid.model_class, only) }
195
+ gids.collect { |gid| GlobalID.parse(gid) }.compact.select { |gid| find_allowed?(gid, only) }
146
196
  end
147
197
 
148
198
  def normalize_app(app)
@@ -154,6 +204,10 @@ class GlobalID
154
204
  @locators = {}
155
205
 
156
206
  class BaseLocator
207
+ def model_class(gid)
208
+ gid.model_name.constantize
209
+ end
210
+
157
211
  def locate(gid, options = {})
158
212
  return unless model_id_is_valid?(gid)
159
213
  model_class = gid.model_class
@@ -223,13 +277,18 @@ class GlobalID
223
277
  end
224
278
  end
225
279
  end
226
- DEFAULT_LOCATOR = UnscopedLocator.new
280
+
281
+ self.default_locator = UnscopedLocator.new
227
282
 
228
283
  class BlockLocator
229
284
  def initialize(block)
230
285
  @locator = block
231
286
  end
232
287
 
288
+ def model_class(gid)
289
+ gid.model_name.constantize
290
+ end
291
+
233
292
  def locate(gid, options = {})
234
293
  @locator.call(gid, options)
235
294
  end
@@ -1,7 +1,5 @@
1
- begin
2
- require 'rails/railtie'
3
- rescue LoadError
4
- else
1
+ # frozen_string_literal: true
2
+ require 'rails'
5
3
  require 'global_id'
6
4
  require 'active_support/core_ext/string/inflections'
7
5
  require 'active_support/core_ext/integer/time'
@@ -48,5 +46,3 @@ class GlobalID
48
46
  end
49
47
  end
50
48
  end
51
-
52
- end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'active_support/message_verifier'
2
3
  require 'time'
3
4
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'uri/generic'
2
3
  require 'active_support/core_ext/module/aliasing'
3
4
  require 'active_support/core_ext/object/blank'
@@ -36,12 +37,15 @@ module URI
36
37
  COMPOSITE_MODEL_ID_MAX_SIZE = 20
37
38
  COMPOSITE_MODEL_ID_DELIMITER = "/"
38
39
 
40
+ URI_PARSER = URI::RFC3986_Parser.new # :nodoc:
41
+
39
42
  class << self
40
- # Validates +app+'s as URI hostnames containing only alphanumeric characters
41
- # and hyphens. An ArgumentError is raised if +app+ is invalid.
43
+ # Validates +app+'s as URI hostnames containing only alphanumeric characters,
44
+ # hyphens and dashes. An ArgumentError is raised if +app+ is invalid.
42
45
  #
43
46
  # URI::GID.validate_app('bcx') # => 'bcx'
44
47
  # URI::GID.validate_app('foo-bar') # => 'foo-bar'
48
+ # URI::GID.validate_app('foo_bar') # => 'foo_bar'
45
49
  #
46
50
  # URI::GID.validate_app(nil) # => ArgumentError
47
51
  # URI::GID.validate_app('foo/bar') # => ArgumentError
@@ -49,7 +53,7 @@ module URI
49
53
  parse("gid://#{app}/Model/1").app
50
54
  rescue URI::Error
51
55
  raise ArgumentError, 'Invalid app name. ' \
52
- 'App names must be valid URI hostnames: alphanumeric and hyphen characters only.'
56
+ 'App names must be valid URI hostnames: alphanumeric, hyphen and underscore characters only.'
53
57
  end
54
58
 
55
59
  # Create a new URI::GID by parsing a gid string with argument check.
@@ -62,7 +66,7 @@ module URI
62
66
  # URI.parse('gid://bcx') # => URI::GID instance
63
67
  # URI::GID.parse('gid://bcx/') # => raises URI::InvalidComponentError
64
68
  def parse(uri)
65
- generic_components = URI.split(uri) << nil << true # nil parser, true arg_check
69
+ generic_components = URI.split(uri) << URI_PARSER << true # RFC3986 parser, true arg_check
66
70
  new(*generic_components)
67
71
  end
68
72
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'active_support/message_verifier'
2
3
 
3
4
  class GlobalID
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ class GlobalID
3
+ VERSION = "1.4.0"
4
+ end
data/lib/global_id.rb CHANGED
@@ -1,4 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  require 'active_support'
3
+ require 'global_id/version'
2
4
  require 'global_id/global_id'
3
5
 
4
6
  autoload :SignedGlobalID, 'global_id/signed_global_id'
data/lib/globalid.rb CHANGED
@@ -1,2 +1,3 @@
1
+ # frozen_string_literal: true
1
2
  require 'global_id'
2
- require 'global_id/railtie'
3
+ require 'global_id/railtie' if defined?(Rails::Railtie)
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: globalid
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-09-05 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activesupport
@@ -38,6 +37,20 @@ dependencies:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
39
  version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: minitest
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '6'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "<"
52
+ - !ruby/object:Gem::Version
53
+ version: '6'
41
54
  description: URIs for your models makes it easy to pass references around.
42
55
  email: david@loudthinking.com
43
56
  executables: []
@@ -55,13 +68,17 @@ files:
55
68
  - lib/global_id/signed_global_id.rb
56
69
  - lib/global_id/uri/gid.rb
57
70
  - lib/global_id/verifier.rb
71
+ - lib/global_id/version.rb
58
72
  - lib/globalid.rb
59
73
  homepage: http://www.rubyonrails.org
60
74
  licenses:
61
75
  - MIT
62
76
  metadata:
77
+ bug_tracker_uri: https://github.com/rails/globalid/issues
78
+ changelog_uri: https://github.com/rails/globalid/releases/tag/v1.4.0
79
+ mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
80
+ source_code_uri: https://github.com/rails/globalid/tree/v1.4.0
63
81
  rubygems_mfa_required: 'true'
64
- post_install_message:
65
82
  rdoc_options: []
66
83
  require_paths:
67
84
  - lib
@@ -69,15 +86,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
69
86
  requirements:
70
87
  - - ">="
71
88
  - !ruby/object:Gem::Version
72
- version: 2.5.0
89
+ version: 2.7.0
73
90
  required_rubygems_version: !ruby/object:Gem::Requirement
74
91
  requirements:
75
92
  - - ">="
76
93
  - !ruby/object:Gem::Version
77
94
  version: '0'
78
95
  requirements: []
79
- rubygems_version: 3.4.1
80
- signing_key:
96
+ rubygems_version: 4.0.12
81
97
  specification_version: 4
82
98
  summary: 'Refer to any model with a URI: gid://app/class/id'
83
99
  test_files: []