universalid 0.0.1
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 +7 -0
- data/README.md +278 -0
- data/Rakefile +14 -0
- data/lib/universal_id/active_model_serializer.rb +53 -0
- data/lib/universal_id/config.rb +18 -0
- data/lib/universal_id/errors.rb +11 -0
- data/lib/universal_id/portable.rb +24 -0
- data/lib/universal_id/portable_hash.rb +85 -0
- data/lib/universal_id/version.rb +5 -0
- data/lib/universal_id.rb +13 -0
- data/lib/universalid.rb +3 -0
- metadata +256 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: '043986f0e3474969c1d0ff0cbf97749d10a4fbb78ccd49296347b9d9e49935b6'
|
4
|
+
data.tar.gz: b43b1fa649dd682d12d8cb8e3fc373aaf2dcd7d98f64c7e36ad91747a130200d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 94768a2128d5b16c7eb2e4989b64c9a9bab603c33f1cf696429c67e8ad4321a888f8df1c68d19f2c74941265a6fa9a6081527970c52a87a1ba325fd7e9558d3a
|
7
|
+
data.tar.gz: fddaae3b8892726c533ea1527cb6040c215057df342ea6542f4ba669878badf270b13a6369b490abc021fe77da4871a0773cdaff093777c511a79c84a4ac3539
|
data/README.md
ADDED
@@ -0,0 +1,278 @@
|
|
1
|
+
<p align="center">
|
2
|
+
<h1 align="center">Universal ID 🌌</h1>
|
3
|
+
<p align="center">
|
4
|
+
<a href="http://blog.codinghorror.com/the-best-code-is-no-code-at-all/">
|
5
|
+
<img alt="Lines of Code" src="https://img.shields.io/badge/loc-165-47d299.svg" />
|
6
|
+
</a>
|
7
|
+
<a href="https://codeclimate.com/github/hopsoft/universalid/maintainability">
|
8
|
+
<img src="https://api.codeclimate.com/v1/badges/80bcd3acced072534a3a/maintainability" />
|
9
|
+
</a>
|
10
|
+
<a href="https://rubygems.org/gems/universalid">
|
11
|
+
<img alt="GEM Version" src="https://img.shields.io/gem/v/universalid?color=168AFE&include_prereleases&logo=ruby&logoColor=FE1616">
|
12
|
+
</a>
|
13
|
+
<a href="https://rubygems.org/gems/universalid">
|
14
|
+
<img alt="GEM Downloads" src="https://img.shields.io/gem/dt/universalid?color=168AFE&logo=ruby&logoColor=FE1616">
|
15
|
+
</a>
|
16
|
+
<a href="https://github.com/testdouble/standard">
|
17
|
+
<img alt="Ruby Style" src="https://img.shields.io/badge/style-standard-168AFE?logo=ruby&logoColor=FE1616" />
|
18
|
+
</a>
|
19
|
+
<a href="https://github.com/hopsoft/universalid/actions/workflows/tests.yml">
|
20
|
+
<img alt="Tests" src="https://github.com/hopsoft/universalid/actions/workflows/tests.yml/badge.svg" />
|
21
|
+
</a>
|
22
|
+
<a href="https://github.com/sponsors/hopsoft">
|
23
|
+
<img alt="Sponsors" src="https://img.shields.io/github/sponsors/hopsoft?color=eb4aaa&logo=GitHub%20Sponsors" />
|
24
|
+
</a>
|
25
|
+
<br>
|
26
|
+
<a href="https://ruby.social/@hopsoft">
|
27
|
+
<img alt="Ruby.Social Follow" src="https://img.shields.io/mastodon/follow/000008274?domain=https%3A%2F%2Fruby.social&label=%40hopsoft&style=social">
|
28
|
+
</a>
|
29
|
+
<a href="https://twitter.com/hopsoft">
|
30
|
+
<img alt="Twitter Follow" src="https://img.shields.io/twitter/url?label=%40hopsoft&style=social&url=https%3A%2F%2Ftwitter.com%2Fhopsoft">
|
31
|
+
</a>
|
32
|
+
</p>
|
33
|
+
<h2 align="center">GlobalID support for Array, Hash, ActiveRecord::Relation, and more.</h2>
|
34
|
+
<h3 align="center">Simple, standardized, secure marshaling for peace-of-mind object portability</h3>
|
35
|
+
</p>
|
36
|
+
|
37
|
+
Portability has never been easier.
|
38
|
+
UniversalID simplifies marshaling by bringing [`GlobalID`'s](https://github.com/rails/globalid) powerful features to more Ruby objects...
|
39
|
+
including unsaved ActiveModels. 🤯
|
40
|
+
|
41
|
+
## Sponsors
|
42
|
+
|
43
|
+
<p align="center">
|
44
|
+
<em>Proudly sponsored by</em>
|
45
|
+
</p>
|
46
|
+
<p align="center">
|
47
|
+
<a href="https://www.clickfunnels.com?utm_source=hopsoft&utm_medium=open-source&utm_campaign=universalid">
|
48
|
+
<img src="https://images.clickfunnel.com/uploads/digital_asset/file/176632/clickfunnels-dark-logo.svg" width="575" />
|
49
|
+
</a>
|
50
|
+
</p>
|
51
|
+
|
52
|
+
<!-- Tocer[start]: Auto-generated, don't remove. -->
|
53
|
+
|
54
|
+
## Table of Contents
|
55
|
+
|
56
|
+
- [What is Global ID?](#what-is-global-id)
|
57
|
+
- [Global ID Examples](#global-id-examples)
|
58
|
+
- [What is Universal ID?](#what-is-universal-id)
|
59
|
+
- [Why Expand Global ID?](#why-expand-global-id)
|
60
|
+
- [Summary of Benefits](#summary-of-benefits)
|
61
|
+
- [GlobalID](#globalid)
|
62
|
+
- [SignedGlobalID](#signedglobalid)
|
63
|
+
- [Hash](#hash)
|
64
|
+
- [ActiveModel](#activemodel)
|
65
|
+
- [Benchmarks](#benchmarks)
|
66
|
+
- [License](#license)
|
67
|
+
|
68
|
+
<!-- Tocer[finish]: Auto-generated, don't remove. -->
|
69
|
+
|
70
|
+
## What is Global ID?
|
71
|
+
|
72
|
+
A GlobalID is an URI that uniquely identifies a model instance.
|
73
|
+
It was designed to make ActiveRecord models portable across process boundaries.
|
74
|
+
For example, passing a model instance as an argument _(from the web server)_ to a background job.
|
75
|
+
They also facilitate use-cases like interleaved search results that mix multiple classes into a single unified result.
|
76
|
+
|
77
|
+
GlobalIDs can also be [signed](https://github.com/rails/globalid#signed-global-ids) and dedicated to a
|
78
|
+
[purpose](https://github.com/rails/globalid#signed-global-ids) with an [expiration](https://github.com/rails/globalid#signed-global-ids) policy.
|
79
|
+
|
80
|
+
### Global ID Examples
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
# Basic GlobalIDs
|
84
|
+
campaign = Campaign.create(name: "Example")
|
85
|
+
campaign.to_gid #............ #<GlobalID:0x000... @uri=#<URI::GID gid://UniversalID/Campaign/1>>
|
86
|
+
campaign.to_gid.uri #........ #<URI::GID gid://UniversalID/Campaign/1>
|
87
|
+
campaign.to_gid.to_s #....... gid://UniversalID/Campaign/1
|
88
|
+
campaign.to_gid_param #...... Z2lkOi8vVW5pdmVyc2FsSUQvQ2FtcGFpZ24vMQ
|
89
|
+
|
90
|
+
gid = GlobalID.parse("Z2lkOi8vVW5pdmVyc2FsSUQvQ2FtcGFpZ24vMQ") # #<GlobalID:0x000... @uri=#<URI::GID gid://UniversalID/Campaign/1>>
|
91
|
+
campaign == gid.find # true
|
92
|
+
```
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
# Signed GlobalIDs
|
96
|
+
campaign = Campaign.create(name: "Example")
|
97
|
+
campaign.to_sgid #......... #<SignedGlobalID:0x000... @expires_at=nil, @purpose="...", @uri=#<URI::GID gid://UniversalID/Campaign/1>, ...>
|
98
|
+
campaign.to_sgid_param #... BAh7CEkiCGdpZAY6BkVUSSIhZ2lkOi8vVW...
|
99
|
+
|
100
|
+
sgid = SignedGlobalID.parse("BAh7CEkiCGdpZAY6BkVUSSIhZ2lkOi8vVW...") # #<SignedGlobalID:0x000... @expires_at=nil, @purpose="...", @uri=#<URI::GID gid://UniversalID/Campaign/1>
|
101
|
+
campaign == sgid.find # true
|
102
|
+
```
|
103
|
+
|
104
|
+
## What is Universal ID?
|
105
|
+
|
106
|
+
UniversalID extends GlobalID functionality to more objects.
|
107
|
+
|
108
|
+
- Array
|
109
|
+
- Hash
|
110
|
+
- ActiveModel _(unsaved)_
|
111
|
+
- ActiveRecord::Relation
|
112
|
+
- etc.
|
113
|
+
|
114
|
+
### Why Expand Global ID?
|
115
|
+
|
116
|
+
A variety of additional use-cases can be handled easily _(with minimal code)_ by extending GlobalID to objects like Hash.
|
117
|
+
Consider a multi-step form or wizard where users incrementally build up a complex set of related ActiveRecord instances.
|
118
|
+
|
119
|
+
- When do we save to the database?
|
120
|
+
- What about validations? Will they be a problem if we save early?
|
121
|
+
- Should we persist the data in cache instead?
|
122
|
+
- What if the user abandons the process?
|
123
|
+
- How do we cleanup abandoned data?
|
124
|
+
- Should we consider full-stack-frontend to manage state client side before saving? 😱
|
125
|
+
|
126
|
+
**Don't fret!** UniversalID supports safely marshaling unsaved ActiveModels between steps.
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
# 1. Start multi-step form (partial data)
|
130
|
+
campaign = Campaign.new(name: "Example") #....... unsaved data
|
131
|
+
param = campaign.to_portable_hash_sgid_param #... make it portable (assign this to a hidden field, querystrig etc.)
|
132
|
+
|
133
|
+
# HTTP request / crossing a process boundary / etc.
|
134
|
+
|
135
|
+
# Step 2. Continue multi-step form (enrich partial data)
|
136
|
+
campaign = Campaign.new_from_portable_hash(param) #................................... unsaved data
|
137
|
+
campaign.emails << campaign.emails.build(subject: "First Email", body: "Welcome") #... unsaved data
|
138
|
+
param = campaign.to_portable_hash_sgid_param #........................................ make it portable
|
139
|
+
|
140
|
+
# HTTP request / crossing a process boundary / etc.
|
141
|
+
|
142
|
+
# Step 3-N. Continue multi-step form (enrich partial data)
|
143
|
+
# ...
|
144
|
+
|
145
|
+
# Final Step. Save the data
|
146
|
+
campaign = Campaign.new_from_portable_hash(param)
|
147
|
+
campaign.save!
|
148
|
+
```
|
149
|
+
|
150
|
+
**And... this is just one use-case!**
|
151
|
+
|
152
|
+
UniversalID can be used to solve a multitude of problems with minimal effort.
|
153
|
+
|
154
|
+
- Sharable artifacts (reports, configurations, etc.)
|
155
|
+
- Data versioning
|
156
|
+
- Digital products
|
157
|
+
- etc. *the only limit is your imagination*
|
158
|
+
|
159
|
+
### Summary of Benefits
|
160
|
+
|
161
|
+
#### GlobalID
|
162
|
+
|
163
|
+
- Standardizes marshaling _(fewer bespoke solutions)_
|
164
|
+
- Reduces complexity and lines-of-code
|
165
|
+
- Simplifies derivative works _(works with existing data-models/object-structures)_
|
166
|
+
- Encapsulates portability across processes and systems _(self-contained)_
|
167
|
+
- Creates opportunity for generic solutions _(meta-programming)_
|
168
|
+
- Minimizes data storage needs for incomplete or ephemeral data _(URL safe string)_
|
169
|
+
- Facilitates easy rollback when incomplete or ephemeral data is abandoned
|
170
|
+
|
171
|
+
#### SignedGlobalID
|
172
|
+
|
173
|
+
- Enhances security _(can't be tampered with, prevents MITM attacks, etc.)_
|
174
|
+
- Provides scoping for a specific purpose _(via `for`)_
|
175
|
+
- Supports versioning _(via purpose)_
|
176
|
+
- Includes scarcity _(via expiration)_
|
177
|
+
- Enables productization _(an SGID string is a digital "product")_
|
178
|
+
|
179
|
+
### Hash
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
hash = {name: "Example", list: [1,2,3], object: {nested: true}}
|
183
|
+
portable = UniversalID::PortableHash.new(hash)
|
184
|
+
gid_param = portable.to_gid_param #..... Z2lkOi8vVW5pdmVyc2FsSUQvVW5pdmVyc2FsSUQ6OlBvcnRhYmxlSGFzaC9lTnFyVnNwTHpFMVZzbEp5clVqTUxj...
|
185
|
+
sgid_param = portable.to_sgid_param #... BAh7CEkiCGdpZAY6BkVUSSIBg2dpZDovL1VuaXZlcnNhbElEL1VuaXZlcnNhbElEOjpQb3J0YWJsZUhhc2gvZU5x...
|
186
|
+
|
187
|
+
|
188
|
+
UniversalID::PortableHash.parse_gid(gid_param).find
|
189
|
+
{"name"=>"Example", "list"=>[1, 2, 3], "object"=>{"nested"=>true}}
|
190
|
+
|
191
|
+
|
192
|
+
UniversalID::PortableHash.parse_gid(sgid_param).find
|
193
|
+
{"name"=>"Example", "list"=>[1, 2, 3], "object"=>{"nested"=>true}}
|
194
|
+
```
|
195
|
+
|
196
|
+
### ActiveModel
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
email = Email.new(subject: "Example", body: "Hi there...") # unsaved
|
200
|
+
gid = email.to_portable_hash_gid_param #..... Z2lkOi8vVW5pdmVyc2FsSUQvVW5pdmVyc2FsSUQ6OlBvcnRhYmxlSGFzaC9lTnFyVmlvdVRjcEtUUz...
|
201
|
+
sgid = email.to_portable_hash_sgid_param #... BAh7CEkiCGdpZAY6BkVUSSJzZ2lkOi8vVW5pdmVyc2FsSUQvVW5pdmVyc2FsSUQ6OlBvcnRhYmxlSG...
|
202
|
+
|
203
|
+
copy = Email.new_from_portable_hash(gid)
|
204
|
+
signed_copy = Email.new_from_portable_hash(sgid)
|
205
|
+
|
206
|
+
email.save!
|
207
|
+
|
208
|
+
options = {portable_hash_options: {except: [:id, :created_at, :updated_at]}}
|
209
|
+
# NOTE: Options can be configured globally via UniversalID.config.portable_hash
|
210
|
+
|
211
|
+
gid = email.to_portable_hash_gid_param(options)
|
212
|
+
sgid = email.to_portable_hash_gid_param(options)
|
213
|
+
|
214
|
+
# Copies are new records and don't include values for id, created_at, or updated_at
|
215
|
+
copy = Email.new_from_portable_hash(gid)
|
216
|
+
signed_copy = Email.new_from_portable_hash(sgid)
|
217
|
+
```
|
218
|
+
|
219
|
+
### Benchmarks
|
220
|
+
|
221
|
+
```
|
222
|
+
# Simple Campaign with 3 associated Email records (nested attributes)
|
223
|
+
==================================================================================================
|
224
|
+
Benchmarking 10000 iterations
|
225
|
+
==================================================================================================
|
226
|
+
user system total real
|
227
|
+
PortableHash.new 0.194428 0.000358 0.194786 ( 0.194788)
|
228
|
+
Average 0.000019 0.000000 0.000019 ( 0.000019)
|
229
|
+
..................................................................................................
|
230
|
+
user system total real
|
231
|
+
PortableHash.new w/ options 0.191249 0.000421 0.191670 ( 0.191677)
|
232
|
+
Average 0.000019 0.000000 0.000019 ( 0.000019)
|
233
|
+
..................................................................................................
|
234
|
+
user system total real
|
235
|
+
PortableHash.find 0.061181 0.002219 0.063400 ( 0.063401)
|
236
|
+
Average 0.000006 0.000000 0.000006 ( 0.000006)
|
237
|
+
..................................................................................................
|
238
|
+
user system total real
|
239
|
+
PortableHash#id 0.342809 0.001674 0.344483 ( 0.344494)
|
240
|
+
Average 0.000034 0.000000 0.000034 ( 0.000034)
|
241
|
+
..................................................................................................
|
242
|
+
user system total real
|
243
|
+
PortableHash#to_gid 0.422914 0.001586 0.424500 ( 0.424498)
|
244
|
+
Average 0.000042 0.000000 0.000042 ( 0.000042)
|
245
|
+
..................................................................................................
|
246
|
+
user system total real
|
247
|
+
PortableHash#to_gid_param 0.437669 0.001826 0.439495 ( 0.439509)
|
248
|
+
Average 0.000044 0.000000 0.000044 ( 0.000044)
|
249
|
+
..................................................................................................
|
250
|
+
user system total real
|
251
|
+
PortableHash#to_sgid 0.430941 0.002055 0.432996 ( 0.433020)
|
252
|
+
Average 0.000043 0.000000 0.000043 ( 0.000043)
|
253
|
+
..................................................................................................
|
254
|
+
user system total real
|
255
|
+
PortableHash#to_sgid_param 0.492965 0.001968 0.494933 ( 0.494978)
|
256
|
+
Average 0.000049 0.000000 0.000049 ( 0.000049)
|
257
|
+
..................................................................................................
|
258
|
+
user system total real
|
259
|
+
ActiveModelSerializer.new_from_portable_hash 3.009667 0.006939 3.016606 ( 3.017388)
|
260
|
+
Average 0.000301 0.000001 0.000302 ( 0.000302)
|
261
|
+
..................................................................................................
|
262
|
+
user system total real
|
263
|
+
ActiveModelSerializer.new_from_portable_hash (signed) 3.189282 0.006650 3.195932 ( 3.196076)
|
264
|
+
Average 0.000319 0.000001 0.000320 ( 0.000320)
|
265
|
+
..................................................................................................
|
266
|
+
user system total real
|
267
|
+
ActiveModelSerializer#to_portable_hash_gid_param 0.923773 0.002653 0.926426 ( 0.926427)
|
268
|
+
Average 0.000092 0.000000 0.000093 ( 0.000093)
|
269
|
+
..................................................................................................
|
270
|
+
user system total real
|
271
|
+
ActiveModelSerializer#to_portable_hash_sgid_param 0.991336 0.002782 0.994118 ( 0.994133)
|
272
|
+
Average 0.000099 0.000000 0.000099 ( 0.000099)
|
273
|
+
..................................................................................................
|
274
|
+
```
|
275
|
+
|
276
|
+
## License
|
277
|
+
|
278
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "minitest/test_task"
|
5
|
+
|
6
|
+
task default: :test
|
7
|
+
|
8
|
+
Minitest::TestTask.create(:test) do |t|
|
9
|
+
t.test_globs = if ARGV.size > 1
|
10
|
+
ARGV[1..]
|
11
|
+
else
|
12
|
+
["test/**/*_test.rb"]
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module UniversalID::ActiveModelSerializer
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include ActiveModel::Serializers::JSON
|
6
|
+
|
7
|
+
class_methods do
|
8
|
+
def new_from_portable_hash(value, options = {})
|
9
|
+
hash = if value.is_a?(UniversalID::PortableHash)
|
10
|
+
value
|
11
|
+
else
|
12
|
+
gid = UniversalID::PortableHash.parse_gid(value.to_s, options)
|
13
|
+
gid&.find || {portable_hash_error: "Invalid or expired UniversalID::PortableHash! #{value.to_s.inspect}"}
|
14
|
+
end
|
15
|
+
new hash
|
16
|
+
rescue => error
|
17
|
+
new(portable_hash_error: "Invalid or expired UniversalID::PortableHash! #{error.inspect}")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
included do
|
22
|
+
validate { errors.add(:base, portable_hash_error) if portable_hash_error }
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_accessor :portable_hash_error
|
26
|
+
|
27
|
+
def to_portable_hash(options = {})
|
28
|
+
portable_hash_options = options.delete(:portable_hash_options)
|
29
|
+
UniversalID::PortableHash.new as_json(options).merge(portable_hash_options: portable_hash_options)
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_portable_hash_global_id(options = {})
|
33
|
+
gid_options = options.delete(:gid_options) || {}
|
34
|
+
to_portable_hash(options).to_gid(gid_options)
|
35
|
+
end
|
36
|
+
alias_method :to_portable_hash_gid, :to_portable_hash_global_id
|
37
|
+
|
38
|
+
def to_portable_hash_global_id_param(options = {})
|
39
|
+
to_portable_hash_gid(options).to_param
|
40
|
+
end
|
41
|
+
alias_method :to_portable_hash_gid_param, :to_portable_hash_global_id_param
|
42
|
+
|
43
|
+
def to_portable_hash_signed_global_id(options = {})
|
44
|
+
gid_options = options.delete(:gid_options) || {}
|
45
|
+
to_portable_hash(options).to_sgid(gid_options)
|
46
|
+
end
|
47
|
+
alias_method :to_portable_hash_sgid, :to_portable_hash_signed_global_id
|
48
|
+
|
49
|
+
def to_portable_hash_signed_global_id_param(options = {})
|
50
|
+
to_portable_hash_sgid(options).to_param
|
51
|
+
end
|
52
|
+
alias_method :to_portable_hash_sgid_param, :to_portable_hash_signed_global_id_param
|
53
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module UniversalID
|
4
|
+
def self.config
|
5
|
+
@config ||= begin
|
6
|
+
options = ActiveSupport::OrderedOptions.new
|
7
|
+
|
8
|
+
# Default options for UniversalID::PortableHash ........................................................
|
9
|
+
options.portable_hash = {
|
10
|
+
allow_blank: false,
|
11
|
+
only: [], # keys to include (trumps except)
|
12
|
+
except: [] # keys to exclude
|
13
|
+
}
|
14
|
+
|
15
|
+
options
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class UniversalID::LocatorError < StandardError
|
4
|
+
attr_reader :id, :cause
|
5
|
+
|
6
|
+
def initialize(id = nil, cause = nil)
|
7
|
+
super "Failed to locate the id #{id.inspect} Cause: #{cause.inspect}"
|
8
|
+
@id = id
|
9
|
+
@cause = cause
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module UniversalID::Portable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include GlobalID::Identification
|
6
|
+
|
7
|
+
class_methods do
|
8
|
+
def config
|
9
|
+
UniversalID.config
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse_gid(gid, options = {})
|
13
|
+
GlobalID.parse(gid, options) || SignedGlobalID.parse(gid, options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def find(id)
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def id
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class UniversalID::PortableHash < Hash
|
4
|
+
include UniversalID::Portable
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def config
|
8
|
+
super.portable_hash.with_indifferent_access
|
9
|
+
end
|
10
|
+
|
11
|
+
def find(id)
|
12
|
+
compressed_json = Base64.urlsafe_decode64(id)
|
13
|
+
JSON.parse Zlib::Inflate.inflate(compressed_json)
|
14
|
+
rescue => error
|
15
|
+
raise UniversalID::LocatorError.new(id, error)
|
16
|
+
end
|
17
|
+
|
18
|
+
def deep_transform(hash, options)
|
19
|
+
include_list = options[:only]
|
20
|
+
exclude_list = options[:except]
|
21
|
+
hash.each_with_object({}) do |(key, value), memo|
|
22
|
+
key = key.to_s
|
23
|
+
next if include_list.any? && incldue_list.none?(key)
|
24
|
+
next if exclude_list.any?(key)
|
25
|
+
transform(value, options: options) { |val| memo[key] = val }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def transform(value, options:)
|
32
|
+
value = case value
|
33
|
+
when Hash then deep_transform(value, options)
|
34
|
+
when Array then value.map { |val| transform(val, options: options) }
|
35
|
+
else value
|
36
|
+
end
|
37
|
+
|
38
|
+
if block_given?
|
39
|
+
yield value if value.present? || options[:allow_blank]
|
40
|
+
end
|
41
|
+
|
42
|
+
value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
delegate :config, to: :"self.class"
|
47
|
+
attr_reader :options
|
48
|
+
|
49
|
+
def initialize(hash)
|
50
|
+
@options = merge_options!(extract_options!(hash))
|
51
|
+
merge! self.class.deep_transform(hash, options)
|
52
|
+
end
|
53
|
+
|
54
|
+
def id
|
55
|
+
compressed_json = Zlib::Deflate.deflate(to_json, Zlib::BEST_COMPRESSION)
|
56
|
+
Base64.urlsafe_encode64 compressed_json, padding: false
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def extract_options!(hash)
|
62
|
+
options = hash.delete(:portable_hash_options) || hash.delete("portable_hash_options") || {}
|
63
|
+
options = options.each_with_object({}) do |(key, val), memo|
|
64
|
+
memo[key.to_sym] = val.is_a?(Array) ? val.map(&:to_s) : val
|
65
|
+
end
|
66
|
+
options.with_indifferent_access
|
67
|
+
end
|
68
|
+
|
69
|
+
def merge_options!(options)
|
70
|
+
config.each do |key, val|
|
71
|
+
default = config[key]
|
72
|
+
custom = options[key]
|
73
|
+
|
74
|
+
if default.is_a?(Array)
|
75
|
+
custom = [] if custom.nil?
|
76
|
+
custom = custom.is_a?(Array) ? custom : [custom]
|
77
|
+
options[key] = (default + custom).uniq
|
78
|
+
else
|
79
|
+
options[key] = custom || default
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
options
|
84
|
+
end
|
85
|
+
end
|
data/lib/universal_id.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
require "zlib"
|
5
|
+
require "globalid"
|
6
|
+
require "active_model"
|
7
|
+
require "active_support/all"
|
8
|
+
require_relative "universal_id/version"
|
9
|
+
require_relative "universal_id/errors"
|
10
|
+
require_relative "universal_id/config"
|
11
|
+
require_relative "universal_id/portable"
|
12
|
+
require_relative "universal_id/portable_hash"
|
13
|
+
require_relative "universal_id/active_model_serializer"
|
data/lib/universalid.rb
ADDED
metadata
ADDED
@@ -0,0 +1,256 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: universalid
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nate Hopkins (hopsoft)
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-04-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activemodel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '6.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '6.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: globalid
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.1'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activerecord
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: awesome_print
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: faker
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: magic_frozen_string_literal
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: minitest-reporters
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: pry-byebug
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: pry-doc
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rake
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: sqlite3
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: standard
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - ">="
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '0'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: tocer
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - ">="
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0'
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - ">="
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '0'
|
209
|
+
description: 'UniversalID expands GlobalID support to objects like Array, Hash, ActiveRecord::Relation,
|
210
|
+
and more.
|
211
|
+
|
212
|
+
'
|
213
|
+
email:
|
214
|
+
- natehop@gmail.com
|
215
|
+
executables: []
|
216
|
+
extensions: []
|
217
|
+
extra_rdoc_files: []
|
218
|
+
files:
|
219
|
+
- README.md
|
220
|
+
- Rakefile
|
221
|
+
- lib/universal_id.rb
|
222
|
+
- lib/universal_id/active_model_serializer.rb
|
223
|
+
- lib/universal_id/config.rb
|
224
|
+
- lib/universal_id/errors.rb
|
225
|
+
- lib/universal_id/portable.rb
|
226
|
+
- lib/universal_id/portable_hash.rb
|
227
|
+
- lib/universal_id/version.rb
|
228
|
+
- lib/universalid.rb
|
229
|
+
homepage: https://github.com/hopsoft/universalid
|
230
|
+
licenses:
|
231
|
+
- MIT
|
232
|
+
metadata:
|
233
|
+
homepage_uri: https://github.com/hopsoft/universalid
|
234
|
+
source_code_uri: https://github.com/hopsoft/universalid
|
235
|
+
changelog_uri: https://github.com/hopsoft/universalid/blob/main/CHANGELOG.md
|
236
|
+
post_install_message:
|
237
|
+
rdoc_options: []
|
238
|
+
require_paths:
|
239
|
+
- lib
|
240
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
241
|
+
requirements:
|
242
|
+
- - ">="
|
243
|
+
- !ruby/object:Gem::Version
|
244
|
+
version: 2.7.5
|
245
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
246
|
+
requirements:
|
247
|
+
- - ">="
|
248
|
+
- !ruby/object:Gem::Version
|
249
|
+
version: '0'
|
250
|
+
requirements: []
|
251
|
+
rubygems_version: 3.3.21
|
252
|
+
signing_key:
|
253
|
+
specification_version: 4
|
254
|
+
summary: GlobalID for for Arrays, Hashes, and objects like ActiveRecord::Relation,
|
255
|
+
etc.
|
256
|
+
test_files: []
|