globalid 1.1.0 → 1.2.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 +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
|