globalid 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +21 -2
- data/lib/global_id/global_id.rb +5 -4
- data/lib/global_id/identification.rb +99 -0
- data/lib/global_id/locator.rb +69 -17
- data/lib/global_id/railtie.rb +4 -0
- data/lib/global_id/signed_global_id.rb +18 -15
- data/lib/global_id/uri/gid.rb +30 -7
- data/lib/global_id/verifier.rb +2 -2
- data/lib/global_id.rb +4 -0
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 15140d3d09fd236ea096c178a473440eb3d2fab55503978d646f3f13a863589c
|
4
|
+
data.tar.gz: 65351562c30c6c4e68d5b21ac8a502a7f7f5c8995cf538844395d123c685afba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2a642776d9233ad70854e49b1dde8287397b94321a158747abc6738c4a976a4bb93a5de9fa72d8e3f7792a5667cc01eb938200b0fb8da675bce28af8488c93d
|
7
|
+
data.tar.gz: c390cbaf842ce68dfd97e4a6d94ae40f9b83fa694ee5931975d96d1b8bd4c8795fbb308a12d3e0ba54f5e8b87d16229a665abe86aa9d00ec0e5e98bb5777ad23
|
data/README.md
CHANGED
@@ -161,6 +161,25 @@ GlobalID::Locator.locate_many gids
|
|
161
161
|
|
162
162
|
Note the order is maintained in the returned results.
|
163
163
|
|
164
|
+
### Options
|
165
|
+
|
166
|
+
Either `GlobalID::Locator.locate` or `GlobalID::Locator.locate_many` supports a hash of options as second parameter. The supported options are:
|
167
|
+
|
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)
|
171
|
+
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
|
174
|
+
allowed to be located. Passing one or more classes limits instances of returned
|
175
|
+
classes to those classes or their subclasses. Passing one or more modules in limits
|
176
|
+
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
|
179
|
+
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,
|
181
|
+
we will use `#where(id: ids)` instead, which does not raise on missing records.
|
182
|
+
|
164
183
|
### Custom App Locator
|
165
184
|
|
166
185
|
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 +191,7 @@ A custom locator can either be a block or a class.
|
|
172
191
|
Using a block:
|
173
192
|
|
174
193
|
```ruby
|
175
|
-
GlobalID::Locator.use :foo do |gid|
|
194
|
+
GlobalID::Locator.use :foo do |gid, options|
|
176
195
|
FooRemote.const_get(gid.model_name).find(gid.model_id)
|
177
196
|
end
|
178
197
|
```
|
@@ -182,7 +201,7 @@ Using a class:
|
|
182
201
|
```ruby
|
183
202
|
GlobalID::Locator.use :bar, BarLocator.new
|
184
203
|
class BarLocator
|
185
|
-
def locate(gid)
|
204
|
+
def locate(gid, options = {})
|
186
205
|
@search_client.search name: gid.model_name, id: gid.model_id
|
187
206
|
end
|
188
207
|
end
|
data/lib/global_id/global_id.rb
CHANGED
@@ -50,12 +50,13 @@ class GlobalID
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def model_class
|
53
|
-
|
53
|
+
@model_class ||= begin
|
54
|
+
model = model_name.constantize
|
54
55
|
|
55
|
-
|
56
|
+
if model <= GlobalID
|
57
|
+
raise ArgumentError, "GlobalID and SignedGlobalID cannot be used as model_class."
|
58
|
+
end
|
56
59
|
model
|
57
|
-
else
|
58
|
-
raise ArgumentError, "GlobalID and SignedGlobalID cannot be used as model_class."
|
59
60
|
end
|
60
61
|
end
|
61
62
|
|
@@ -1,19 +1,118 @@
|
|
1
1
|
class GlobalID
|
2
|
+
# Mix `GlobalID::Identification` into any model with a `#find(id)` class
|
3
|
+
# method. Support is automatically included in Active Record.
|
4
|
+
#
|
5
|
+
# class Person
|
6
|
+
# include ActiveModel::Model
|
7
|
+
# include GlobalID::Identification
|
8
|
+
#
|
9
|
+
# attr_accessor :id
|
10
|
+
#
|
11
|
+
# def self.find(id)
|
12
|
+
# new id: id
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# def ==(other)
|
16
|
+
# id == other.try(:id)
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# person_gid = Person.find(1).to_global_id
|
21
|
+
# # => #<GlobalID ...
|
22
|
+
# person_gid.uri
|
23
|
+
# # => #<URI ...
|
24
|
+
# person_gid.to_s
|
25
|
+
# # => "gid://app/Person/1"
|
26
|
+
# GlobalID::Locator.locate person_gid
|
27
|
+
# # => #<Person:0x007fae94bf6298 @id="1">
|
2
28
|
module Identification
|
29
|
+
|
30
|
+
# Returns the Global ID of the model.
|
31
|
+
#
|
32
|
+
# model = Person.new id: 1
|
33
|
+
# global_id = model.to_global_id
|
34
|
+
# global_id.modal_class # => Person
|
35
|
+
# global_id.modal_id # => "1"
|
36
|
+
# global_id.to_param # => "Z2lkOi8vYm9yZGZvbGlvL1BlcnNvbi8x"
|
3
37
|
def to_global_id(options = {})
|
4
38
|
GlobalID.create(self, options)
|
5
39
|
end
|
6
40
|
alias to_gid to_global_id
|
7
41
|
|
42
|
+
# Returns the Global ID parameter of the model.
|
43
|
+
#
|
44
|
+
# model = Person.new id: 1
|
45
|
+
# model.to_gid_param # => ""Z2lkOi8vYm9yZGZvbGlvL1BlcnNvbi8x"
|
8
46
|
def to_gid_param(options = {})
|
9
47
|
to_global_id(options).to_param
|
10
48
|
end
|
11
49
|
|
50
|
+
# Returns the Signed Global ID of the model.
|
51
|
+
# Signed Global IDs ensure that the data hasn't been tampered with.
|
52
|
+
#
|
53
|
+
# model = Person.new id: 1
|
54
|
+
# signed_global_id = model.to_signed_global_id
|
55
|
+
# signed_global_id.modal_class # => Person
|
56
|
+
# signed_global_id.modal_id # => "1"
|
57
|
+
# signed_global_id.to_param # => "BAh7CEkiCGdpZAY6BkVUSSIiZ2..."
|
58
|
+
#
|
59
|
+
# ==== Expiration
|
60
|
+
#
|
61
|
+
# Signed Global IDs can expire some time in the future. This is useful if
|
62
|
+
# there's a resource people shouldn't have indefinite access to, like a
|
63
|
+
# share link.
|
64
|
+
#
|
65
|
+
# expiring_sgid = Document.find(5).to_sgid(expires_in: 2.hours, for: 'sharing')
|
66
|
+
# # => #<SignedGlobalID:0x008fde45df8937 ...>
|
67
|
+
# # Within 2 hours...
|
68
|
+
# GlobalID::Locator.locate_signed(expiring_sgid.to_s, for: 'sharing')
|
69
|
+
# # => #<Document:0x007fae94bf6298 @id="5">
|
70
|
+
# # More than 2 hours later...
|
71
|
+
# GlobalID::Locator.locate_signed(expiring_sgid.to_s, for: 'sharing')
|
72
|
+
# # => nil
|
73
|
+
#
|
74
|
+
# In Rails, an auto-expiry of 1 month is set by default.
|
75
|
+
#
|
76
|
+
# You need to explicitly pass `expires_in: nil` to generate a permanent
|
77
|
+
# SGID that will not expire,
|
78
|
+
#
|
79
|
+
# never_expiring_sgid = Document.find(5).to_sgid(expires_in: nil)
|
80
|
+
# # => #<SignedGlobalID:0x008fde45df8937 ...>
|
81
|
+
#
|
82
|
+
# # Any time later...
|
83
|
+
# GlobalID::Locator.locate_signed never_expiring_sgid
|
84
|
+
# # => #<Document:0x007fae94bf6298 @id="5">
|
85
|
+
#
|
86
|
+
# It's also possible to pass a specific expiry time
|
87
|
+
#
|
88
|
+
# explicit_expiring_sgid = SecretAgentMessage.find(5).to_sgid(expires_at: Time.now.advance(hours: 1))
|
89
|
+
# # => #<SignedGlobalID:0x008fde45df8937 ...>
|
90
|
+
#
|
91
|
+
# # 1 hour later...
|
92
|
+
# GlobalID::Locator.locate_signed explicit_expiring_sgid.to_s
|
93
|
+
# # => nil
|
94
|
+
#
|
95
|
+
# Note that an explicit `:expires_at` takes precedence over a relative `:expires_in`.
|
96
|
+
#
|
97
|
+
# ==== Purpose
|
98
|
+
#
|
99
|
+
# You can even bump the security up some more by explaining what purpose a
|
100
|
+
# Signed Global ID is for. In this way evildoers can't reuse a sign-up
|
101
|
+
# form's SGID on the login page. For example.
|
102
|
+
#
|
103
|
+
# signup_person_sgid = Person.find(1).to_sgid(for: 'signup_form')
|
104
|
+
# # => #<SignedGlobalID:0x007fea1984b520
|
105
|
+
# GlobalID::Locator.locate_signed(signup_person_sgid.to_s, for: 'signup_form')
|
106
|
+
# => #<Person:0x007fae94bf6298 @id="1">
|
12
107
|
def to_signed_global_id(options = {})
|
13
108
|
SignedGlobalID.create(self, options)
|
14
109
|
end
|
15
110
|
alias to_sgid to_signed_global_id
|
16
111
|
|
112
|
+
# Returns the Signed Global ID parameter.
|
113
|
+
#
|
114
|
+
# model = Person.new id: 1
|
115
|
+
# model.to_sgid_param # => "BAh7CEkiCGdpZAY6BkVUSSIiZ2..."
|
17
116
|
def to_sgid_param(options = {})
|
18
117
|
to_signed_global_id(options).to_param
|
19
118
|
end
|
data/lib/global_id/locator.rb
CHANGED
@@ -2,18 +2,33 @@ require 'active_support/core_ext/enumerable' # For Enumerable#index_by
|
|
2
2
|
|
3
3
|
class GlobalID
|
4
4
|
module Locator
|
5
|
+
class InvalidModelIdError < StandardError; end
|
6
|
+
|
5
7
|
class << self
|
6
8
|
# Takes either a GlobalID or a string that can be turned into a GlobalID
|
7
9
|
#
|
8
10
|
# Options:
|
11
|
+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them.
|
12
|
+
# The same structure you would pass into a +includes+ method of Active Record.
|
13
|
+
# If present, locate will load all the relationships specified here.
|
14
|
+
# See https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations.
|
9
15
|
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
|
10
16
|
# allowed to be located. Passing one or more classes limits instances of returned
|
11
17
|
# classes to those classes or their subclasses. Passing one or more modules in limits
|
12
18
|
# instances of returned classes to those including that module. If no classes or
|
13
19
|
# modules match, +nil+ is returned.
|
14
20
|
def locate(gid, options = {})
|
15
|
-
|
16
|
-
|
21
|
+
gid = GlobalID.parse(gid)
|
22
|
+
|
23
|
+
return unless gid && find_allowed?(gid.model_class, options[:only])
|
24
|
+
|
25
|
+
locator = locator_for(gid)
|
26
|
+
|
27
|
+
if locator.method(:locate).arity == 1
|
28
|
+
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 = {})`."
|
29
|
+
locator.locate(gid)
|
30
|
+
else
|
31
|
+
locator.locate(gid, options.except(:only))
|
17
32
|
end
|
18
33
|
end
|
19
34
|
|
@@ -28,6 +43,11 @@ class GlobalID
|
|
28
43
|
# per model class, but still interpolate the results to match the order in which the gids were passed.
|
29
44
|
#
|
30
45
|
# Options:
|
46
|
+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them
|
47
|
+
# The same structure you would pass into a includes method of Active Record.
|
48
|
+
# @see https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
|
49
|
+
# If present, locate_many will load all the relationships specified here.
|
50
|
+
# Note: It only works if all the gids models have that relationships.
|
31
51
|
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
|
32
52
|
# allowed to be located. Passing one or more classes limits instances of returned
|
33
53
|
# classes to those classes or their subclasses. Passing one or more modules in limits
|
@@ -49,6 +69,10 @@ class GlobalID
|
|
49
69
|
# Takes either a SignedGlobalID or a string that can be turned into a SignedGlobalID
|
50
70
|
#
|
51
71
|
# Options:
|
72
|
+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them
|
73
|
+
# The same structure you would pass into a includes method of Active Record.
|
74
|
+
# @see https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
|
75
|
+
# If present, locate_signed will load all the relationships specified here.
|
52
76
|
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
|
53
77
|
# allowed to be located. Passing one or more classes limits instances of returned
|
54
78
|
# classes to those classes or their subclasses. Passing one or more modules in limits
|
@@ -66,6 +90,11 @@ class GlobalID
|
|
66
90
|
# the results to match the order in which the gids were passed.
|
67
91
|
#
|
68
92
|
# Options:
|
93
|
+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them
|
94
|
+
# The same structure you would pass into a includes method of Active Record.
|
95
|
+
# @see https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
|
96
|
+
# If present, locate_many_signed will load all the relationships specified here.
|
97
|
+
# Note: It only works if all the gids models have that relationships.
|
69
98
|
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
|
70
99
|
# allowed to be located. Passing one or more classes limits instances of returned
|
71
100
|
# classes to those classes or their subclasses. Passing one or more modules in limits
|
@@ -82,7 +111,7 @@ class GlobalID
|
|
82
111
|
#
|
83
112
|
# Using a block:
|
84
113
|
#
|
85
|
-
# GlobalID::Locator.use :foo do |gid|
|
114
|
+
# GlobalID::Locator.use :foo do |gid, options|
|
86
115
|
# FooRemote.const_get(gid.model_name).find(gid.model_id)
|
87
116
|
# end
|
88
117
|
#
|
@@ -91,7 +120,7 @@ class GlobalID
|
|
91
120
|
# GlobalID::Locator.use :bar, BarLocator.new
|
92
121
|
#
|
93
122
|
# class BarLocator
|
94
|
-
# def locate(gid)
|
123
|
+
# def locate(gid, options = {})
|
95
124
|
# @search_client.search name: gid.model_name, id: gid.model_id
|
96
125
|
# end
|
97
126
|
# end
|
@@ -125,32 +154,55 @@ class GlobalID
|
|
125
154
|
@locators = {}
|
126
155
|
|
127
156
|
class BaseLocator
|
128
|
-
def locate(gid)
|
129
|
-
|
157
|
+
def locate(gid, options = {})
|
158
|
+
return unless model_id_is_valid?(gid)
|
159
|
+
model_class = gid.model_class
|
160
|
+
model_class = model_class.includes(options[:includes]) if options[:includes]
|
161
|
+
|
162
|
+
model_class.find gid.model_id
|
130
163
|
end
|
131
164
|
|
132
165
|
def locate_many(gids, options = {})
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
166
|
+
ids_by_model = Hash.new { |hash, key| hash[key] = [] }
|
167
|
+
|
168
|
+
gids.each do |gid|
|
169
|
+
next unless model_id_is_valid?(gid)
|
170
|
+
ids_by_model[gid.model_class] << gid.model_id
|
171
|
+
end
|
172
|
+
|
173
|
+
records_by_model_name_and_id = {}
|
138
174
|
|
139
|
-
|
175
|
+
ids_by_model.each do |model, ids|
|
176
|
+
records = find_records(model, ids, ignore_missing: options[:ignore_missing], includes: options[:includes])
|
177
|
+
|
178
|
+
records_by_id = records.index_by do |record|
|
179
|
+
record.id.is_a?(Array) ? record.id.map(&:to_s) : record.id.to_s
|
180
|
+
end
|
181
|
+
|
182
|
+
records_by_model_name_and_id[model.name] = records_by_id
|
183
|
+
end
|
184
|
+
|
185
|
+
gids.filter_map { |gid| records_by_model_name_and_id[gid.model_name][gid.model_id] }
|
140
186
|
end
|
141
187
|
|
142
188
|
private
|
143
189
|
def find_records(model_class, ids, options)
|
190
|
+
model_class = model_class.includes(options[:includes]) if options[:includes]
|
191
|
+
|
144
192
|
if options[:ignore_missing]
|
145
|
-
model_class.where(
|
193
|
+
model_class.where(model_class.primary_key => ids)
|
146
194
|
else
|
147
195
|
model_class.find(ids)
|
148
196
|
end
|
149
197
|
end
|
198
|
+
|
199
|
+
def model_id_is_valid?(gid)
|
200
|
+
Array(gid.model_id).size == Array(gid.model_class.primary_key).size
|
201
|
+
end
|
150
202
|
end
|
151
203
|
|
152
204
|
class UnscopedLocator < BaseLocator
|
153
|
-
def locate(gid)
|
205
|
+
def locate(gid, options = {})
|
154
206
|
unscoped(gid.model_class) { super }
|
155
207
|
end
|
156
208
|
|
@@ -174,12 +226,12 @@ class GlobalID
|
|
174
226
|
@locator = block
|
175
227
|
end
|
176
228
|
|
177
|
-
def locate(gid)
|
178
|
-
@locator.call(gid)
|
229
|
+
def locate(gid, options = {})
|
230
|
+
@locator.call(gid, options)
|
179
231
|
end
|
180
232
|
|
181
233
|
def locate_many(gids, options = {})
|
182
|
-
gids.map { |gid| locate(gid) }
|
234
|
+
gids.map { |gid| locate(gid, options) }
|
183
235
|
end
|
184
236
|
end
|
185
237
|
end
|
data/lib/global_id/railtie.rb
CHANGED
@@ -5,7 +5,7 @@ class SignedGlobalID < GlobalID
|
|
5
5
|
class ExpiredMessage < StandardError; end
|
6
6
|
|
7
7
|
class << self
|
8
|
-
attr_accessor :verifier
|
8
|
+
attr_accessor :verifier, :expires_in
|
9
9
|
|
10
10
|
def parse(sgid, options = {})
|
11
11
|
super verify(sgid.to_s, options), options
|
@@ -19,8 +19,6 @@ class SignedGlobalID < GlobalID
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
-
attr_accessor :expires_in
|
23
|
-
|
24
22
|
DEFAULT_PURPOSE = "default"
|
25
23
|
|
26
24
|
def pick_purpose(options)
|
@@ -29,11 +27,22 @@ class SignedGlobalID < GlobalID
|
|
29
27
|
|
30
28
|
private
|
31
29
|
def verify(sgid, options)
|
30
|
+
verify_with_verifier_validated_metadata(sgid, options) ||
|
31
|
+
verify_with_legacy_self_validated_metadata(sgid, options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def verify_with_verifier_validated_metadata(sgid, options)
|
35
|
+
pick_verifier(options).verify(sgid, purpose: pick_purpose(options))
|
36
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def verify_with_legacy_self_validated_metadata(sgid, options)
|
32
41
|
metadata = pick_verifier(options).verify(sgid)
|
33
42
|
|
34
43
|
raise_if_expired(metadata['expires_at'])
|
35
44
|
|
36
|
-
metadata['gid'] if pick_purpose(options) == metadata['purpose']
|
45
|
+
metadata['gid'] if pick_purpose(options)&.to_s == metadata['purpose']&.to_s
|
37
46
|
rescue ActiveSupport::MessageVerifier::InvalidSignature, ExpiredMessage
|
38
47
|
nil
|
39
48
|
end
|
@@ -55,25 +64,19 @@ class SignedGlobalID < GlobalID
|
|
55
64
|
end
|
56
65
|
|
57
66
|
def to_s
|
58
|
-
@sgid ||= @verifier.generate(
|
67
|
+
@sgid ||= @verifier.generate(@uri.to_s, purpose: purpose, expires_at: expires_at)
|
59
68
|
end
|
60
69
|
alias to_param to_s
|
61
70
|
|
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
71
|
def ==(other)
|
69
72
|
super && @purpose == other.purpose
|
70
73
|
end
|
71
74
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
end
|
75
|
+
def inspect # :nodoc:
|
76
|
+
"#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
|
77
|
+
end
|
76
78
|
|
79
|
+
private
|
77
80
|
def pick_expiration(options)
|
78
81
|
return options[:expires_at] if options.key?(:expires_at)
|
79
82
|
|
data/lib/global_id/uri/gid.rb
CHANGED
@@ -30,6 +30,11 @@ module URI
|
|
30
30
|
|
31
31
|
# Raised when creating a Global ID for a model without an id
|
32
32
|
class MissingModelIdError < URI::InvalidComponentError; end
|
33
|
+
class InvalidModelIdError < URI::InvalidComponentError; end
|
34
|
+
|
35
|
+
# Maximum size of a model id segment
|
36
|
+
COMPOSITE_MODEL_ID_MAX_SIZE = 20
|
37
|
+
COMPOSITE_MODEL_ID_DELIMITER = "/"
|
33
38
|
|
34
39
|
class << self
|
35
40
|
# Validates +app+'s as URI hostnames containing only alphanumeric characters
|
@@ -83,7 +88,8 @@ module URI
|
|
83
88
|
def build(args)
|
84
89
|
parts = Util.make_components_hash(self, args)
|
85
90
|
parts[:host] = parts[:app]
|
86
|
-
|
91
|
+
model_id_segment = Array(parts[:model_id]).map { |p| CGI.escape(p.to_s) }.join(COMPOSITE_MODEL_ID_DELIMITER)
|
92
|
+
parts[:path] = "/#{parts[:model_name]}/#{model_id_segment}"
|
87
93
|
|
88
94
|
if parts[:params] && !parts[:params].empty?
|
89
95
|
parts[:query] = URI.encode_www_form(parts[:params])
|
@@ -147,12 +153,22 @@ module URI
|
|
147
153
|
|
148
154
|
def set_model_components(path, validate = false)
|
149
155
|
_, 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
156
|
|
157
|
+
validate_component(model_name) && validate_model_id_section(model_id, model_name) if validate
|
154
158
|
@model_name = model_name
|
155
|
-
|
159
|
+
|
160
|
+
if model_id
|
161
|
+
model_id_parts = model_id
|
162
|
+
.split(COMPOSITE_MODEL_ID_DELIMITER, COMPOSITE_MODEL_ID_MAX_SIZE)
|
163
|
+
.reject(&:blank?)
|
164
|
+
|
165
|
+
model_id_parts.map! do |id|
|
166
|
+
validate_model_id(id)
|
167
|
+
CGI.unescape(id)
|
168
|
+
end
|
169
|
+
|
170
|
+
@model_id = model_id_parts.length == 1 ? model_id_parts.first : model_id_parts
|
171
|
+
end
|
156
172
|
end
|
157
173
|
|
158
174
|
def validate_component(component)
|
@@ -162,13 +178,20 @@ module URI
|
|
162
178
|
"Expected a URI like gid://app/Person/1234: #{inspect}"
|
163
179
|
end
|
164
180
|
|
165
|
-
def
|
166
|
-
return model_id unless model_id.blank?
|
181
|
+
def validate_model_id_section(model_id, model_name)
|
182
|
+
return model_id unless model_id.blank?
|
167
183
|
|
168
184
|
raise MissingModelIdError, "Unable to create a Global ID for " \
|
169
185
|
"#{model_name} without a model id."
|
170
186
|
end
|
171
187
|
|
188
|
+
def validate_model_id(model_id_part)
|
189
|
+
return unless model_id_part.include?('/')
|
190
|
+
|
191
|
+
raise InvalidModelIdError, "Unable to create a Global ID for " \
|
192
|
+
"#{model_name} with a malformed model id."
|
193
|
+
end
|
194
|
+
|
172
195
|
def parse_query_params(query)
|
173
196
|
Hash[URI.decode_www_form(query)].with_indifferent_access if query
|
174
197
|
end
|
data/lib/global_id/verifier.rb
CHANGED
@@ -3,11 +3,11 @@ require 'active_support/message_verifier'
|
|
3
3
|
class GlobalID
|
4
4
|
class Verifier < ActiveSupport::MessageVerifier
|
5
5
|
private
|
6
|
-
def encode(data)
|
6
|
+
def encode(data, **)
|
7
7
|
::Base64.urlsafe_encode64(data)
|
8
8
|
end
|
9
9
|
|
10
|
-
def decode(data)
|
10
|
+
def decode(data, **)
|
11
11
|
::Base64.urlsafe_decode64(data)
|
12
12
|
end
|
13
13
|
end
|
data/lib/global_id.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: globalid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Heinemeier Hansson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-09-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '6.1'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '6.1'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|