globalid 1.1.0 → 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 +4 -4
- data/README.md +112 -8
- data/lib/global_id/global_id.rb +24 -5
- data/lib/global_id/identification.rb +100 -0
- data/lib/global_id/locator.rb +138 -23
- data/lib/global_id/railtie.rb +6 -6
- data/lib/global_id/signed_global_id.rb +19 -15
- data/lib/global_id/uri/gid.rb +38 -11
- data/lib/global_id/verifier.rb +3 -2
- data/lib/global_id/version.rb +4 -0
- data/lib/global_id.rb +6 -0
- data/lib/globalid.rb +2 -1
- metadata +25 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d57eeec2571a0ec07f96c46e09a1f2513958e798069847b6001c08088ebcb69c
|
|
4
|
+
data.tar.gz: d6585f62af8ae463201fdc6994d9f816c71e793dffb00803561e53bde284711e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
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
|
|
|
@@ -161,6 +178,25 @@ GlobalID::Locator.locate_many gids
|
|
|
161
178
|
|
|
162
179
|
Note the order is maintained in the returned results.
|
|
163
180
|
|
|
181
|
+
### Options
|
|
182
|
+
|
|
183
|
+
Either `GlobalID::Locator.locate` or `GlobalID::Locator.locate_many` supports a hash of options as second parameter. The supported options are:
|
|
184
|
+
|
|
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).
|
|
188
|
+
If present, `locate` or `locate_many` will eager load all the relationships specified here.
|
|
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
|
|
191
|
+
allowed to be located. Passing one or more classes limits instances of returned
|
|
192
|
+
classes to those classes or their subclasses. Passing one or more modules in limits
|
|
193
|
+
instances of returned classes to those including that module. If no classes or
|
|
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
|
|
196
|
+
ids extracted from the GIDs. In Active Record (and other data stores following the same pattern),
|
|
197
|
+
`#find` will raise an exception if a named ID can't be found. When you set this option to `true`,
|
|
198
|
+
we will use `#where(id: ids)` instead, which does not raise on missing records.
|
|
199
|
+
|
|
164
200
|
### Custom App Locator
|
|
165
201
|
|
|
166
202
|
A custom locator can be set for an app by calling `GlobalID::Locator.use` and providing an app locator to use for that app.
|
|
@@ -172,7 +208,7 @@ A custom locator can either be a block or a class.
|
|
|
172
208
|
Using a block:
|
|
173
209
|
|
|
174
210
|
```ruby
|
|
175
|
-
GlobalID::Locator.use :foo do |gid|
|
|
211
|
+
GlobalID::Locator.use :foo do |gid, options|
|
|
176
212
|
FooRemote.const_get(gid.model_name).find(gid.model_id)
|
|
177
213
|
end
|
|
178
214
|
```
|
|
@@ -180,17 +216,85 @@ end
|
|
|
180
216
|
Using a class:
|
|
181
217
|
|
|
182
218
|
```ruby
|
|
183
|
-
GlobalID::Locator.use :bar, BarLocator.new
|
|
184
219
|
class BarLocator
|
|
185
|
-
def locate(gid)
|
|
220
|
+
def locate(gid, options = {})
|
|
186
221
|
@search_client.search name: gid.model_name, id: gid.model_id
|
|
187
222
|
end
|
|
188
223
|
end
|
|
224
|
+
|
|
225
|
+
GlobalID::Locator.use :bar, BarLocator.new
|
|
189
226
|
```
|
|
190
227
|
|
|
191
|
-
|
|
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
|
|
238
|
+
```
|
|
239
|
+
|
|
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.
|
|
192
241
|
Other apps will still keep using the default locator.
|
|
193
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
|
+
|
|
194
298
|
## Contributing to GlobalID
|
|
195
299
|
|
|
196
300
|
GlobalID is work of many contributors. You're encouraged to submit pull requests, propose
|
data/lib/global_id/global_id.rb
CHANGED
|
@@ -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
|
|
@@ -50,12 +55,26 @@ class GlobalID
|
|
|
50
55
|
end
|
|
51
56
|
|
|
52
57
|
def model_class
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
@model_class ||= begin
|
|
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
|
|
74
|
+
if model <= GlobalID
|
|
75
|
+
raise ArgumentError, "GlobalID and SignedGlobalID cannot be used as model_class."
|
|
76
|
+
end
|
|
56
77
|
model
|
|
57
|
-
else
|
|
58
|
-
raise ArgumentError, "GlobalID and SignedGlobalID cannot be used as model_class."
|
|
59
78
|
end
|
|
60
79
|
end
|
|
61
80
|
|
|
@@ -1,19 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
class GlobalID
|
|
3
|
+
# Mix `GlobalID::Identification` into any model with a `#find(id)` class
|
|
4
|
+
# method. Support is automatically included in Active Record.
|
|
5
|
+
#
|
|
6
|
+
# class Person
|
|
7
|
+
# include ActiveModel::Model
|
|
8
|
+
# include GlobalID::Identification
|
|
9
|
+
#
|
|
10
|
+
# attr_accessor :id
|
|
11
|
+
#
|
|
12
|
+
# def self.find(id)
|
|
13
|
+
# new id: id
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# def ==(other)
|
|
17
|
+
# id == other.try(:id)
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# person_gid = Person.find(1).to_global_id
|
|
22
|
+
# # => #<GlobalID ...
|
|
23
|
+
# person_gid.uri
|
|
24
|
+
# # => #<URI ...
|
|
25
|
+
# person_gid.to_s
|
|
26
|
+
# # => "gid://app/Person/1"
|
|
27
|
+
# GlobalID::Locator.locate person_gid
|
|
28
|
+
# # => #<Person:0x007fae94bf6298 @id="1">
|
|
2
29
|
module Identification
|
|
30
|
+
|
|
31
|
+
# Returns the Global ID of the model.
|
|
32
|
+
#
|
|
33
|
+
# model = Person.new id: 1
|
|
34
|
+
# global_id = model.to_global_id
|
|
35
|
+
# global_id.model_class # => Person
|
|
36
|
+
# global_id.model_id # => "1"
|
|
37
|
+
# global_id.to_param # => "Z2lkOi8vYm9yZGZvbGlvL1BlcnNvbi8x"
|
|
3
38
|
def to_global_id(options = {})
|
|
4
39
|
GlobalID.create(self, options)
|
|
5
40
|
end
|
|
6
41
|
alias to_gid to_global_id
|
|
7
42
|
|
|
43
|
+
# Returns the Global ID parameter of the model.
|
|
44
|
+
#
|
|
45
|
+
# model = Person.new id: 1
|
|
46
|
+
# model.to_gid_param # => ""Z2lkOi8vYm9yZGZvbGlvL1BlcnNvbi8x"
|
|
8
47
|
def to_gid_param(options = {})
|
|
9
48
|
to_global_id(options).to_param
|
|
10
49
|
end
|
|
11
50
|
|
|
51
|
+
# Returns the Signed Global ID of the model.
|
|
52
|
+
# Signed Global IDs ensure that the data hasn't been tampered with.
|
|
53
|
+
#
|
|
54
|
+
# model = Person.new id: 1
|
|
55
|
+
# signed_global_id = model.to_signed_global_id
|
|
56
|
+
# signed_global_id.model_class # => Person
|
|
57
|
+
# signed_global_id.model_id # => "1"
|
|
58
|
+
# signed_global_id.to_param # => "BAh7CEkiCGdpZAY6BkVUSSIiZ2..."
|
|
59
|
+
#
|
|
60
|
+
# ==== Expiration
|
|
61
|
+
#
|
|
62
|
+
# Signed Global IDs can expire some time in the future. This is useful if
|
|
63
|
+
# there's a resource people shouldn't have indefinite access to, like a
|
|
64
|
+
# share link.
|
|
65
|
+
#
|
|
66
|
+
# expiring_sgid = Document.find(5).to_sgid(expires_in: 2.hours, for: 'sharing')
|
|
67
|
+
# # => #<SignedGlobalID:0x008fde45df8937 ...>
|
|
68
|
+
# # Within 2 hours...
|
|
69
|
+
# GlobalID::Locator.locate_signed(expiring_sgid.to_s, for: 'sharing')
|
|
70
|
+
# # => #<Document:0x007fae94bf6298 @id="5">
|
|
71
|
+
# # More than 2 hours later...
|
|
72
|
+
# GlobalID::Locator.locate_signed(expiring_sgid.to_s, for: 'sharing')
|
|
73
|
+
# # => nil
|
|
74
|
+
#
|
|
75
|
+
# In Rails, an auto-expiry of 1 month is set by default.
|
|
76
|
+
#
|
|
77
|
+
# You need to explicitly pass `expires_in: nil` to generate a permanent
|
|
78
|
+
# SGID that will not expire,
|
|
79
|
+
#
|
|
80
|
+
# never_expiring_sgid = Document.find(5).to_sgid(expires_in: nil)
|
|
81
|
+
# # => #<SignedGlobalID:0x008fde45df8937 ...>
|
|
82
|
+
#
|
|
83
|
+
# # Any time later...
|
|
84
|
+
# GlobalID::Locator.locate_signed never_expiring_sgid
|
|
85
|
+
# # => #<Document:0x007fae94bf6298 @id="5">
|
|
86
|
+
#
|
|
87
|
+
# It's also possible to pass a specific expiry time
|
|
88
|
+
#
|
|
89
|
+
# explicit_expiring_sgid = SecretAgentMessage.find(5).to_sgid(expires_at: Time.now.advance(hours: 1))
|
|
90
|
+
# # => #<SignedGlobalID:0x008fde45df8937 ...>
|
|
91
|
+
#
|
|
92
|
+
# # 1 hour later...
|
|
93
|
+
# GlobalID::Locator.locate_signed explicit_expiring_sgid.to_s
|
|
94
|
+
# # => nil
|
|
95
|
+
#
|
|
96
|
+
# Note that an explicit `:expires_at` takes precedence over a relative `:expires_in`.
|
|
97
|
+
#
|
|
98
|
+
# ==== Purpose
|
|
99
|
+
#
|
|
100
|
+
# You can even bump the security up some more by explaining what purpose a
|
|
101
|
+
# Signed Global ID is for. In this way evildoers can't reuse a sign-up
|
|
102
|
+
# form's SGID on the login page. For example.
|
|
103
|
+
#
|
|
104
|
+
# signup_person_sgid = Person.find(1).to_sgid(for: 'signup_form')
|
|
105
|
+
# # => #<SignedGlobalID:0x007fea1984b520
|
|
106
|
+
# GlobalID::Locator.locate_signed(signup_person_sgid.to_s, for: 'signup_form')
|
|
107
|
+
# => #<Person:0x007fae94bf6298 @id="1">
|
|
12
108
|
def to_signed_global_id(options = {})
|
|
13
109
|
SignedGlobalID.create(self, options)
|
|
14
110
|
end
|
|
15
111
|
alias to_sgid to_signed_global_id
|
|
16
112
|
|
|
113
|
+
# Returns the Signed Global ID parameter.
|
|
114
|
+
#
|
|
115
|
+
# model = Person.new id: 1
|
|
116
|
+
# model.to_sgid_param # => "BAh7CEkiCGdpZAY6BkVUSSIiZ2..."
|
|
17
117
|
def to_sgid_param(options = {})
|
|
18
118
|
to_signed_global_id(options).to_param
|
|
19
119
|
end
|
data/lib/global_id/locator.rb
CHANGED
|
@@ -1,22 +1,81 @@
|
|
|
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
|
|
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
|
|
18
|
+
|
|
5
19
|
class << self
|
|
20
|
+
# The default locator used when no app-specific locator is found.
|
|
21
|
+
attr_accessor :default_locator
|
|
22
|
+
|
|
6
23
|
# Takes either a GlobalID or a string that can be turned into a GlobalID
|
|
7
24
|
#
|
|
8
25
|
# Options:
|
|
26
|
+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them.
|
|
27
|
+
# The same structure you would pass into a +includes+ method of Active Record.
|
|
28
|
+
# If present, locate will load all the relationships specified here.
|
|
29
|
+
# See https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations.
|
|
9
30
|
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
|
|
10
31
|
# allowed to be located. Passing one or more classes limits instances of returned
|
|
11
32
|
# classes to those classes or their subclasses. Passing one or more modules in limits
|
|
12
33
|
# instances of returned classes to those including that module. If no classes or
|
|
13
34
|
# modules match, +nil+ is returned.
|
|
14
35
|
def locate(gid, options = {})
|
|
15
|
-
|
|
16
|
-
|
|
36
|
+
gid = GlobalID.parse(gid)
|
|
37
|
+
|
|
38
|
+
return unless gid && find_allowed?(gid, options[:only])
|
|
39
|
+
|
|
40
|
+
locator = locator_for(gid)
|
|
41
|
+
|
|
42
|
+
if locator.method(:locate).arity == 1
|
|
43
|
+
GlobalID.deprecator.warn "It seems your locator is defining the `locate` method only with one argument. Please make sure your locator is receiving the options argument as well, like `locate(gid, options = {})`."
|
|
44
|
+
locator.locate(gid)
|
|
45
|
+
else
|
|
46
|
+
locator.locate(gid, options.except(:only))
|
|
17
47
|
end
|
|
18
48
|
end
|
|
19
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
|
+
|
|
20
79
|
# Takes an array of GlobalIDs or strings that can be turned into a GlobalIDs.
|
|
21
80
|
# All GlobalIDs must belong to the same app, as they will be located using
|
|
22
81
|
# the same locator using its locate_many method.
|
|
@@ -28,6 +87,11 @@ class GlobalID
|
|
|
28
87
|
# per model class, but still interpolate the results to match the order in which the gids were passed.
|
|
29
88
|
#
|
|
30
89
|
# Options:
|
|
90
|
+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them
|
|
91
|
+
# The same structure you would pass into a includes method of Active Record.
|
|
92
|
+
# @see https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
|
|
93
|
+
# If present, locate_many will load all the relationships specified here.
|
|
94
|
+
# Note: It only works if all the gids models have that relationships.
|
|
31
95
|
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
|
|
32
96
|
# allowed to be located. Passing one or more classes limits instances of returned
|
|
33
97
|
# classes to those classes or their subclasses. Passing one or more modules in limits
|
|
@@ -49,6 +113,10 @@ class GlobalID
|
|
|
49
113
|
# Takes either a SignedGlobalID or a string that can be turned into a SignedGlobalID
|
|
50
114
|
#
|
|
51
115
|
# Options:
|
|
116
|
+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them
|
|
117
|
+
# The same structure you would pass into a includes method of Active Record.
|
|
118
|
+
# @see https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
|
|
119
|
+
# If present, locate_signed will load all the relationships specified here.
|
|
52
120
|
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
|
|
53
121
|
# allowed to be located. Passing one or more classes limits instances of returned
|
|
54
122
|
# classes to those classes or their subclasses. Passing one or more modules in limits
|
|
@@ -66,6 +134,11 @@ class GlobalID
|
|
|
66
134
|
# the results to match the order in which the gids were passed.
|
|
67
135
|
#
|
|
68
136
|
# Options:
|
|
137
|
+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them
|
|
138
|
+
# The same structure you would pass into a includes method of Active Record.
|
|
139
|
+
# @see https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
|
|
140
|
+
# If present, locate_many_signed will load all the relationships specified here.
|
|
141
|
+
# Note: It only works if all the gids models have that relationships.
|
|
69
142
|
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
|
|
70
143
|
# allowed to be located. Passing one or more classes limits instances of returned
|
|
71
144
|
# classes to those classes or their subclasses. Passing one or more modules in limits
|
|
@@ -82,7 +155,7 @@ class GlobalID
|
|
|
82
155
|
#
|
|
83
156
|
# Using a block:
|
|
84
157
|
#
|
|
85
|
-
# GlobalID::Locator.use :foo do |gid|
|
|
158
|
+
# GlobalID::Locator.use :foo do |gid, options|
|
|
86
159
|
# FooRemote.const_get(gid.model_name).find(gid.model_id)
|
|
87
160
|
# end
|
|
88
161
|
#
|
|
@@ -91,7 +164,7 @@ class GlobalID
|
|
|
91
164
|
# GlobalID::Locator.use :bar, BarLocator.new
|
|
92
165
|
#
|
|
93
166
|
# class BarLocator
|
|
94
|
-
# def locate(gid)
|
|
167
|
+
# def locate(gid, options = {})
|
|
95
168
|
# @search_client.search name: gid.model_name, id: gid.model_id
|
|
96
169
|
# end
|
|
97
170
|
# end
|
|
@@ -103,17 +176,23 @@ class GlobalID
|
|
|
103
176
|
@locators[normalize_app(app)] = locator || BlockLocator.new(locator_block)
|
|
104
177
|
end
|
|
105
178
|
|
|
179
|
+
def locator_for(gid)
|
|
180
|
+
@locators.fetch(normalize_app(gid.app)) { default_locator }
|
|
181
|
+
end
|
|
182
|
+
|
|
106
183
|
private
|
|
107
|
-
def
|
|
108
|
-
|
|
184
|
+
def find_allowed?(gid, only = nil)
|
|
185
|
+
only ? Array(only).any? { |c| gid.model_class <= c } : true
|
|
109
186
|
end
|
|
110
187
|
|
|
111
|
-
def
|
|
112
|
-
|
|
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}"
|
|
113
192
|
end
|
|
114
193
|
|
|
115
194
|
def parse_allowed(gids, only = nil)
|
|
116
|
-
gids.collect { |gid| GlobalID.parse(gid) }.compact.select { |gid| find_allowed?(gid
|
|
195
|
+
gids.collect { |gid| GlobalID.parse(gid) }.compact.select { |gid| find_allowed?(gid, only) }
|
|
117
196
|
end
|
|
118
197
|
|
|
119
198
|
def normalize_app(app)
|
|
@@ -125,32 +204,63 @@ class GlobalID
|
|
|
125
204
|
@locators = {}
|
|
126
205
|
|
|
127
206
|
class BaseLocator
|
|
128
|
-
def
|
|
129
|
-
gid.
|
|
207
|
+
def model_class(gid)
|
|
208
|
+
gid.model_name.constantize
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def locate(gid, options = {})
|
|
212
|
+
return unless model_id_is_valid?(gid)
|
|
213
|
+
model_class = gid.model_class
|
|
214
|
+
model_class = model_class.includes(options[:includes]) if options[:includes]
|
|
215
|
+
|
|
216
|
+
model_class.find gid.model_id
|
|
130
217
|
end
|
|
131
218
|
|
|
132
219
|
def locate_many(gids, options = {})
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
220
|
+
ids_by_model = Hash.new { |hash, key| hash[key] = [] }
|
|
221
|
+
|
|
222
|
+
gids.each do |gid|
|
|
223
|
+
next unless model_id_is_valid?(gid)
|
|
224
|
+
ids_by_model[gid.model_class] << gid.model_id
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
records_by_model_name_and_id = {}
|
|
228
|
+
|
|
229
|
+
ids_by_model.each do |model, ids|
|
|
230
|
+
records = find_records(model, ids, ignore_missing: options[:ignore_missing], includes: options[:includes])
|
|
231
|
+
|
|
232
|
+
records_by_id = records.index_by do |record|
|
|
233
|
+
record.id.is_a?(Array) ? record.id.map(&:to_s) : record.id.to_s
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
records_by_model_name_and_id[model.name] = records_by_id
|
|
237
|
+
end
|
|
138
238
|
|
|
139
|
-
|
|
239
|
+
gids.filter_map { |gid| records_by_model_name_and_id[gid.model_name][gid.model_id] }
|
|
140
240
|
end
|
|
141
241
|
|
|
142
242
|
private
|
|
143
243
|
def find_records(model_class, ids, options)
|
|
244
|
+
model_class = model_class.includes(options[:includes]) if options[:includes]
|
|
245
|
+
|
|
144
246
|
if options[:ignore_missing]
|
|
145
|
-
model_class.where(
|
|
247
|
+
model_class.where(primary_key(model_class) => ids)
|
|
146
248
|
else
|
|
147
249
|
model_class.find(ids)
|
|
148
250
|
end
|
|
149
251
|
end
|
|
252
|
+
|
|
253
|
+
def model_id_is_valid?(gid)
|
|
254
|
+
Array(gid.model_id).size == Array(primary_key(gid.model_class)).size
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def primary_key(model_class)
|
|
258
|
+
model_class.respond_to?(:primary_key) ? model_class.primary_key : :id
|
|
259
|
+
end
|
|
150
260
|
end
|
|
151
261
|
|
|
152
262
|
class UnscopedLocator < BaseLocator
|
|
153
|
-
def locate(gid)
|
|
263
|
+
def locate(gid, options = {})
|
|
154
264
|
unscoped(gid.model_class) { super }
|
|
155
265
|
end
|
|
156
266
|
|
|
@@ -167,19 +277,24 @@ class GlobalID
|
|
|
167
277
|
end
|
|
168
278
|
end
|
|
169
279
|
end
|
|
170
|
-
|
|
280
|
+
|
|
281
|
+
self.default_locator = UnscopedLocator.new
|
|
171
282
|
|
|
172
283
|
class BlockLocator
|
|
173
284
|
def initialize(block)
|
|
174
285
|
@locator = block
|
|
175
286
|
end
|
|
176
287
|
|
|
177
|
-
def
|
|
178
|
-
|
|
288
|
+
def model_class(gid)
|
|
289
|
+
gid.model_name.constantize
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def locate(gid, options = {})
|
|
293
|
+
@locator.call(gid, options)
|
|
179
294
|
end
|
|
180
295
|
|
|
181
296
|
def locate_many(gids, options = {})
|
|
182
|
-
gids.map { |gid| locate(gid) }
|
|
297
|
+
gids.map { |gid| locate(gid, options) }
|
|
183
298
|
end
|
|
184
299
|
end
|
|
185
300
|
end
|
data/lib/global_id/railtie.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
require 'rails
|
|
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'
|
|
@@ -42,7 +40,9 @@ class GlobalID
|
|
|
42
40
|
send :extend, GlobalID::FixtureSet
|
|
43
41
|
end
|
|
44
42
|
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
43
|
|
|
44
|
+
initializer "web_console.deprecator" do |app|
|
|
45
|
+
app.deprecators[:global_id] = GlobalID.deprecator if app.respond_to?(:deprecators)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
48
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require 'active_support/message_verifier'
|
|
2
3
|
require 'time'
|
|
3
4
|
|
|
@@ -5,7 +6,7 @@ class SignedGlobalID < GlobalID
|
|
|
5
6
|
class ExpiredMessage < StandardError; end
|
|
6
7
|
|
|
7
8
|
class << self
|
|
8
|
-
attr_accessor :verifier
|
|
9
|
+
attr_accessor :verifier, :expires_in
|
|
9
10
|
|
|
10
11
|
def parse(sgid, options = {})
|
|
11
12
|
super verify(sgid.to_s, options), options
|
|
@@ -19,8 +20,6 @@ class SignedGlobalID < GlobalID
|
|
|
19
20
|
end
|
|
20
21
|
end
|
|
21
22
|
|
|
22
|
-
attr_accessor :expires_in
|
|
23
|
-
|
|
24
23
|
DEFAULT_PURPOSE = "default"
|
|
25
24
|
|
|
26
25
|
def pick_purpose(options)
|
|
@@ -29,11 +28,22 @@ class SignedGlobalID < GlobalID
|
|
|
29
28
|
|
|
30
29
|
private
|
|
31
30
|
def verify(sgid, options)
|
|
31
|
+
verify_with_verifier_validated_metadata(sgid, options) ||
|
|
32
|
+
verify_with_legacy_self_validated_metadata(sgid, options)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def verify_with_verifier_validated_metadata(sgid, options)
|
|
36
|
+
pick_verifier(options).verify(sgid, purpose: pick_purpose(options))
|
|
37
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def verify_with_legacy_self_validated_metadata(sgid, options)
|
|
32
42
|
metadata = pick_verifier(options).verify(sgid)
|
|
33
43
|
|
|
34
44
|
raise_if_expired(metadata['expires_at'])
|
|
35
45
|
|
|
36
|
-
metadata['gid'] if pick_purpose(options) == metadata['purpose']
|
|
46
|
+
metadata['gid'] if pick_purpose(options)&.to_s == metadata['purpose']&.to_s
|
|
37
47
|
rescue ActiveSupport::MessageVerifier::InvalidSignature, ExpiredMessage
|
|
38
48
|
nil
|
|
39
49
|
end
|
|
@@ -55,25 +65,19 @@ class SignedGlobalID < GlobalID
|
|
|
55
65
|
end
|
|
56
66
|
|
|
57
67
|
def to_s
|
|
58
|
-
@sgid ||= @verifier.generate(
|
|
68
|
+
@sgid ||= @verifier.generate(@uri.to_s, purpose: purpose, expires_at: expires_at)
|
|
59
69
|
end
|
|
60
70
|
alias to_param to_s
|
|
61
71
|
|
|
62
|
-
def to_h
|
|
63
|
-
# Some serializers decodes symbol keys to symbols, others to strings.
|
|
64
|
-
# Using string keys remedies that.
|
|
65
|
-
{ 'gid' => @uri.to_s, 'purpose' => purpose, 'expires_at' => encoded_expiration }
|
|
66
|
-
end
|
|
67
|
-
|
|
68
72
|
def ==(other)
|
|
69
73
|
super && @purpose == other.purpose
|
|
70
74
|
end
|
|
71
75
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
end
|
|
76
|
+
def inspect # :nodoc:
|
|
77
|
+
"#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
|
|
78
|
+
end
|
|
76
79
|
|
|
80
|
+
private
|
|
77
81
|
def pick_expiration(options)
|
|
78
82
|
return options[:expires_at] if options.key?(:expires_at)
|
|
79
83
|
|
data/lib/global_id/uri/gid.rb
CHANGED
|
@@ -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'
|
|
@@ -30,13 +31,21 @@ module URI
|
|
|
30
31
|
|
|
31
32
|
# Raised when creating a Global ID for a model without an id
|
|
32
33
|
class MissingModelIdError < URI::InvalidComponentError; end
|
|
34
|
+
class InvalidModelIdError < URI::InvalidComponentError; end
|
|
35
|
+
|
|
36
|
+
# Maximum size of a model id segment
|
|
37
|
+
COMPOSITE_MODEL_ID_MAX_SIZE = 20
|
|
38
|
+
COMPOSITE_MODEL_ID_DELIMITER = "/"
|
|
39
|
+
|
|
40
|
+
URI_PARSER = URI::RFC3986_Parser.new # :nodoc:
|
|
33
41
|
|
|
34
42
|
class << self
|
|
35
|
-
# Validates +app+'s as URI hostnames containing only alphanumeric characters
|
|
36
|
-
# and
|
|
43
|
+
# Validates +app+'s as URI hostnames containing only alphanumeric characters,
|
|
44
|
+
# hyphens and dashes. An ArgumentError is raised if +app+ is invalid.
|
|
37
45
|
#
|
|
38
46
|
# URI::GID.validate_app('bcx') # => 'bcx'
|
|
39
47
|
# URI::GID.validate_app('foo-bar') # => 'foo-bar'
|
|
48
|
+
# URI::GID.validate_app('foo_bar') # => 'foo_bar'
|
|
40
49
|
#
|
|
41
50
|
# URI::GID.validate_app(nil) # => ArgumentError
|
|
42
51
|
# URI::GID.validate_app('foo/bar') # => ArgumentError
|
|
@@ -44,7 +53,7 @@ module URI
|
|
|
44
53
|
parse("gid://#{app}/Model/1").app
|
|
45
54
|
rescue URI::Error
|
|
46
55
|
raise ArgumentError, 'Invalid app name. ' \
|
|
47
|
-
'App names must be valid URI hostnames: alphanumeric and
|
|
56
|
+
'App names must be valid URI hostnames: alphanumeric, hyphen and underscore characters only.'
|
|
48
57
|
end
|
|
49
58
|
|
|
50
59
|
# Create a new URI::GID by parsing a gid string with argument check.
|
|
@@ -57,7 +66,7 @@ module URI
|
|
|
57
66
|
# URI.parse('gid://bcx') # => URI::GID instance
|
|
58
67
|
# URI::GID.parse('gid://bcx/') # => raises URI::InvalidComponentError
|
|
59
68
|
def parse(uri)
|
|
60
|
-
generic_components = URI.split(uri) <<
|
|
69
|
+
generic_components = URI.split(uri) << URI_PARSER << true # RFC3986 parser, true arg_check
|
|
61
70
|
new(*generic_components)
|
|
62
71
|
end
|
|
63
72
|
|
|
@@ -83,7 +92,8 @@ module URI
|
|
|
83
92
|
def build(args)
|
|
84
93
|
parts = Util.make_components_hash(self, args)
|
|
85
94
|
parts[:host] = parts[:app]
|
|
86
|
-
|
|
95
|
+
model_id_segment = Array(parts[:model_id]).map { |p| CGI.escape(p.to_s) }.join(COMPOSITE_MODEL_ID_DELIMITER)
|
|
96
|
+
parts[:path] = "/#{parts[:model_name]}/#{model_id_segment}"
|
|
87
97
|
|
|
88
98
|
if parts[:params] && !parts[:params].empty?
|
|
89
99
|
parts[:query] = URI.encode_www_form(parts[:params])
|
|
@@ -147,12 +157,22 @@ module URI
|
|
|
147
157
|
|
|
148
158
|
def set_model_components(path, validate = false)
|
|
149
159
|
_, model_name, model_id = path.split('/', 3)
|
|
150
|
-
validate_component(model_name) && validate_model_id(model_id, model_name) if validate
|
|
151
|
-
|
|
152
|
-
model_id = CGI.unescape(model_id) if model_id
|
|
153
160
|
|
|
161
|
+
validate_component(model_name) && validate_model_id_section(model_id, model_name) if validate
|
|
154
162
|
@model_name = model_name
|
|
155
|
-
|
|
163
|
+
|
|
164
|
+
if model_id
|
|
165
|
+
model_id_parts = model_id
|
|
166
|
+
.split(COMPOSITE_MODEL_ID_DELIMITER, COMPOSITE_MODEL_ID_MAX_SIZE)
|
|
167
|
+
.reject(&:blank?)
|
|
168
|
+
|
|
169
|
+
model_id_parts.map! do |id|
|
|
170
|
+
validate_model_id(id)
|
|
171
|
+
CGI.unescape(id)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
@model_id = model_id_parts.length == 1 ? model_id_parts.first : model_id_parts
|
|
175
|
+
end
|
|
156
176
|
end
|
|
157
177
|
|
|
158
178
|
def validate_component(component)
|
|
@@ -162,13 +182,20 @@ module URI
|
|
|
162
182
|
"Expected a URI like gid://app/Person/1234: #{inspect}"
|
|
163
183
|
end
|
|
164
184
|
|
|
165
|
-
def
|
|
166
|
-
return model_id unless model_id.blank?
|
|
185
|
+
def validate_model_id_section(model_id, model_name)
|
|
186
|
+
return model_id unless model_id.blank?
|
|
167
187
|
|
|
168
188
|
raise MissingModelIdError, "Unable to create a Global ID for " \
|
|
169
189
|
"#{model_name} without a model id."
|
|
170
190
|
end
|
|
171
191
|
|
|
192
|
+
def validate_model_id(model_id_part)
|
|
193
|
+
return unless model_id_part.include?('/')
|
|
194
|
+
|
|
195
|
+
raise InvalidModelIdError, "Unable to create a Global ID for " \
|
|
196
|
+
"#{model_name} with a malformed model id."
|
|
197
|
+
end
|
|
198
|
+
|
|
172
199
|
def parse_query_params(query)
|
|
173
200
|
Hash[URI.decode_www_form(query)].with_indifferent_access if query
|
|
174
201
|
end
|
data/lib/global_id/verifier.rb
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require 'active_support/message_verifier'
|
|
2
3
|
|
|
3
4
|
class GlobalID
|
|
4
5
|
class Verifier < ActiveSupport::MessageVerifier
|
|
5
6
|
private
|
|
6
|
-
def encode(data)
|
|
7
|
+
def encode(data, **)
|
|
7
8
|
::Base64.urlsafe_encode64(data)
|
|
8
9
|
end
|
|
9
10
|
|
|
10
|
-
def decode(data)
|
|
11
|
+
def decode(data, **)
|
|
11
12
|
::Base64.urlsafe_decode64(data)
|
|
12
13
|
end
|
|
13
14
|
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'
|
|
@@ -16,4 +18,8 @@ class GlobalID
|
|
|
16
18
|
super
|
|
17
19
|
require 'global_id/signed_global_id'
|
|
18
20
|
end
|
|
21
|
+
|
|
22
|
+
def self.deprecator # :nodoc:
|
|
23
|
+
@deprecator ||= ActiveSupport::Deprecation.new("2.1", "GlobalID")
|
|
24
|
+
end
|
|
19
25
|
end
|
data/lib/globalid.rb
CHANGED
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.
|
|
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:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activesupport
|
|
@@ -16,14 +15,14 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
18
|
+
version: '6.1'
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
25
|
+
version: '6.1'
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
27
|
name: rake
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -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.
|
|
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:
|
|
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: []
|