globalid 1.3.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 +65 -2
- data/lib/global_id/global_id.rb +16 -2
- data/lib/global_id/identification.rb +1 -0
- data/lib/global_id/locator.rb +61 -6
- data/lib/global_id/railtie.rb +2 -6
- data/lib/global_id/signed_global_id.rb +1 -0
- data/lib/global_id/uri/gid.rb +7 -5
- data/lib/global_id/verifier.rb +1 -0
- data/lib/global_id/version.rb +4 -0
- data/lib/global_id.rb +2 -0
- data/lib/globalid.rb +2 -1
- metadata +19 -4
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.
|
|
@@ -199,17 +216,63 @@ 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
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
|
+
|
|
213
276
|
### Custom Default Locator
|
|
214
277
|
|
|
215
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.
|
|
@@ -221,7 +284,7 @@ class MyCustomLocator < UnscopedLocator
|
|
|
221
284
|
super(gid, options)
|
|
222
285
|
end
|
|
223
286
|
end
|
|
224
|
-
|
|
287
|
+
|
|
225
288
|
def locate_many(gids, options = {})
|
|
226
289
|
ActiveRecord::Base.connected_to(role: :reading) do
|
|
227
290
|
super(gids, options)
|
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
|
|
@@ -55,8 +56,21 @@ class GlobalID
|
|
|
55
56
|
|
|
56
57
|
def model_class
|
|
57
58
|
@model_class ||= begin
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
60
74
|
if model <= GlobalID
|
|
61
75
|
raise ArgumentError, "GlobalID and SignedGlobalID cannot be used as model_class."
|
|
62
76
|
end
|
data/lib/global_id/locator.rb
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
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
|
|
8
20
|
# The default locator used when no app-specific locator is found.
|
|
@@ -23,7 +35,7 @@ class GlobalID
|
|
|
23
35
|
def locate(gid, options = {})
|
|
24
36
|
gid = GlobalID.parse(gid)
|
|
25
37
|
|
|
26
|
-
return unless gid && find_allowed?(gid
|
|
38
|
+
return unless gid && find_allowed?(gid, options[:only])
|
|
27
39
|
|
|
28
40
|
locator = locator_for(gid)
|
|
29
41
|
|
|
@@ -35,6 +47,35 @@ class GlobalID
|
|
|
35
47
|
end
|
|
36
48
|
end
|
|
37
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
|
+
|
|
38
79
|
# Takes an array of GlobalIDs or strings that can be turned into a GlobalIDs.
|
|
39
80
|
# All GlobalIDs must belong to the same app, as they will be located using
|
|
40
81
|
# the same locator using its locate_many method.
|
|
@@ -135,17 +176,23 @@ class GlobalID
|
|
|
135
176
|
@locators[normalize_app(app)] = locator || BlockLocator.new(locator_block)
|
|
136
177
|
end
|
|
137
178
|
|
|
179
|
+
def locator_for(gid)
|
|
180
|
+
@locators.fetch(normalize_app(gid.app)) { default_locator }
|
|
181
|
+
end
|
|
182
|
+
|
|
138
183
|
private
|
|
139
|
-
def
|
|
140
|
-
|
|
184
|
+
def find_allowed?(gid, only = nil)
|
|
185
|
+
only ? Array(only).any? { |c| gid.model_class <= c } : true
|
|
141
186
|
end
|
|
142
187
|
|
|
143
|
-
def
|
|
144
|
-
|
|
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}"
|
|
145
192
|
end
|
|
146
193
|
|
|
147
194
|
def parse_allowed(gids, only = nil)
|
|
148
|
-
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) }
|
|
149
196
|
end
|
|
150
197
|
|
|
151
198
|
def normalize_app(app)
|
|
@@ -157,6 +204,10 @@ class GlobalID
|
|
|
157
204
|
@locators = {}
|
|
158
205
|
|
|
159
206
|
class BaseLocator
|
|
207
|
+
def model_class(gid)
|
|
208
|
+
gid.model_name.constantize
|
|
209
|
+
end
|
|
210
|
+
|
|
160
211
|
def locate(gid, options = {})
|
|
161
212
|
return unless model_id_is_valid?(gid)
|
|
162
213
|
model_class = gid.model_class
|
|
@@ -234,6 +285,10 @@ class GlobalID
|
|
|
234
285
|
@locator = block
|
|
235
286
|
end
|
|
236
287
|
|
|
288
|
+
def model_class(gid)
|
|
289
|
+
gid.model_name.constantize
|
|
290
|
+
end
|
|
291
|
+
|
|
237
292
|
def locate(gid, options = {})
|
|
238
293
|
@locator.call(gid, options)
|
|
239
294
|
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'
|
|
@@ -48,5 +46,3 @@ class GlobalID
|
|
|
48
46
|
end
|
|
49
47
|
end
|
|
50
48
|
end
|
|
51
|
-
|
|
52
|
-
end
|
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'
|
|
@@ -36,14 +37,15 @@ module URI
|
|
|
36
37
|
COMPOSITE_MODEL_ID_MAX_SIZE = 20
|
|
37
38
|
COMPOSITE_MODEL_ID_DELIMITER = "/"
|
|
38
39
|
|
|
39
|
-
URI_PARSER = URI::
|
|
40
|
+
URI_PARSER = URI::RFC3986_Parser.new # :nodoc:
|
|
40
41
|
|
|
41
42
|
class << self
|
|
42
|
-
# Validates +app+'s as URI hostnames containing only alphanumeric characters
|
|
43
|
-
# and
|
|
43
|
+
# Validates +app+'s as URI hostnames containing only alphanumeric characters,
|
|
44
|
+
# hyphens and dashes. An ArgumentError is raised if +app+ is invalid.
|
|
44
45
|
#
|
|
45
46
|
# URI::GID.validate_app('bcx') # => 'bcx'
|
|
46
47
|
# URI::GID.validate_app('foo-bar') # => 'foo-bar'
|
|
48
|
+
# URI::GID.validate_app('foo_bar') # => 'foo_bar'
|
|
47
49
|
#
|
|
48
50
|
# URI::GID.validate_app(nil) # => ArgumentError
|
|
49
51
|
# URI::GID.validate_app('foo/bar') # => ArgumentError
|
|
@@ -51,7 +53,7 @@ module URI
|
|
|
51
53
|
parse("gid://#{app}/Model/1").app
|
|
52
54
|
rescue URI::Error
|
|
53
55
|
raise ArgumentError, 'Invalid app name. ' \
|
|
54
|
-
'App names must be valid URI hostnames: alphanumeric and
|
|
56
|
+
'App names must be valid URI hostnames: alphanumeric, hyphen and underscore characters only.'
|
|
55
57
|
end
|
|
56
58
|
|
|
57
59
|
# Create a new URI::GID by parsing a gid string with argument check.
|
|
@@ -64,7 +66,7 @@ module URI
|
|
|
64
66
|
# URI.parse('gid://bcx') # => URI::GID instance
|
|
65
67
|
# URI::GID.parse('gid://bcx/') # => raises URI::InvalidComponentError
|
|
66
68
|
def parse(uri)
|
|
67
|
-
generic_components = URI.split(uri) << URI_PARSER << true #
|
|
69
|
+
generic_components = URI.split(uri) << URI_PARSER << true # RFC3986 parser, true arg_check
|
|
68
70
|
new(*generic_components)
|
|
69
71
|
end
|
|
70
72
|
|
data/lib/global_id/verifier.rb
CHANGED
data/lib/global_id.rb
CHANGED
data/lib/globalid.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
|
|
@@ -37,6 +37,20 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
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'
|
|
40
54
|
description: URIs for your models makes it easy to pass references around.
|
|
41
55
|
email: david@loudthinking.com
|
|
42
56
|
executables: []
|
|
@@ -54,15 +68,16 @@ files:
|
|
|
54
68
|
- lib/global_id/signed_global_id.rb
|
|
55
69
|
- lib/global_id/uri/gid.rb
|
|
56
70
|
- lib/global_id/verifier.rb
|
|
71
|
+
- lib/global_id/version.rb
|
|
57
72
|
- lib/globalid.rb
|
|
58
73
|
homepage: http://www.rubyonrails.org
|
|
59
74
|
licenses:
|
|
60
75
|
- MIT
|
|
61
76
|
metadata:
|
|
62
77
|
bug_tracker_uri: https://github.com/rails/globalid/issues
|
|
63
|
-
changelog_uri: https://github.com/rails/globalid/releases/tag/v1.
|
|
78
|
+
changelog_uri: https://github.com/rails/globalid/releases/tag/v1.4.0
|
|
64
79
|
mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
|
|
65
|
-
source_code_uri: https://github.com/rails/globalid/tree/v1.
|
|
80
|
+
source_code_uri: https://github.com/rails/globalid/tree/v1.4.0
|
|
66
81
|
rubygems_mfa_required: 'true'
|
|
67
82
|
rdoc_options: []
|
|
68
83
|
require_paths:
|
|
@@ -78,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
78
93
|
- !ruby/object:Gem::Version
|
|
79
94
|
version: '0'
|
|
80
95
|
requirements: []
|
|
81
|
-
rubygems_version:
|
|
96
|
+
rubygems_version: 4.0.12
|
|
82
97
|
specification_version: 4
|
|
83
98
|
summary: 'Refer to any model with a URI: gid://app/class/id'
|
|
84
99
|
test_files: []
|