universalid 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +929 -188
  3. data/Rakefile +1 -5
  4. data/config/default.yml +12 -0
  5. data/config/example.yml +45 -0
  6. data/contrib/active_record/base_message_pack_type.rb +11 -0
  7. data/contrib/active_record/base_packer.rb +130 -0
  8. data/contrib/active_record/base_unpacker.rb +52 -0
  9. data/contrib/active_record/relation_message_pack_type.rb +16 -0
  10. data/contrib/active_record.rb +8 -0
  11. data/contrib/active_support/time_with_zone_message_pack_type.rb +14 -0
  12. data/contrib/active_support.rb +7 -0
  13. data/contrib/global_id/global_id_model.rb +24 -0
  14. data/contrib/global_id/global_id_uid_extension.rb +36 -0
  15. data/contrib/global_id/message_pack_type.rb +15 -0
  16. data/contrib/global_id.rb +7 -0
  17. data/contrib/signed_global_id/message_pack_type.rb +8 -0
  18. data/contrib/signed_global_id.rb +7 -0
  19. data/contrib/tags +75 -0
  20. data/lib/universal_id/contrib.rb +14 -0
  21. data/lib/universal_id/encoder.rb +27 -0
  22. data/lib/universal_id/message_pack_factory.rb +37 -0
  23. data/lib/universal_id/message_pack_types/ruby/composites/open_struct.rb +8 -0
  24. data/lib/universal_id/message_pack_types/ruby/composites/set.rb +9 -0
  25. data/lib/universal_id/message_pack_types/ruby/composites/struct.rb +23 -0
  26. data/lib/universal_id/message_pack_types/ruby/scalars/complex.rb +8 -0
  27. data/lib/universal_id/message_pack_types/ruby/scalars/date.rb +8 -0
  28. data/lib/universal_id/message_pack_types/ruby/scalars/date_time.rb +8 -0
  29. data/lib/universal_id/message_pack_types/ruby/scalars/range.rb +20 -0
  30. data/lib/universal_id/message_pack_types/ruby/scalars/rational.rb +8 -0
  31. data/lib/universal_id/message_pack_types/ruby/scalars/regexp.rb +15 -0
  32. data/lib/universal_id/message_pack_types/uri/uid/type.rb +8 -0
  33. data/lib/universal_id/message_pack_types.rb +26 -0
  34. data/lib/universal_id/prepack_database_options.rb +63 -0
  35. data/lib/universal_id/prepack_options.rb +74 -0
  36. data/lib/universal_id/prepacker.rb +28 -0
  37. data/lib/universal_id/refinements/array_refinement.rb +17 -0
  38. data/lib/universal_id/refinements/hash_refinement.rb +19 -0
  39. data/lib/universal_id/refinements/kernel_refinement.rb +19 -0
  40. data/lib/universal_id/refinements/open_struct_refinement.rb +12 -0
  41. data/lib/universal_id/refinements/set_refinement.rb +12 -0
  42. data/lib/universal_id/refinements.rb +9 -0
  43. data/lib/universal_id/settings.rb +82 -0
  44. data/lib/universal_id/version.rb +1 -1
  45. data/lib/universal_id.rb +23 -10
  46. data/lib/uri/uid.rb +95 -0
  47. metadata +106 -28
  48. data/lib/universal_id/active_model_serializer.rb +0 -53
  49. data/lib/universal_id/config.rb +0 -18
  50. data/lib/universal_id/errors.rb +0 -11
  51. data/lib/universal_id/portable.rb +0 -24
  52. data/lib/universal_id/portable_hash.rb +0 -85
data/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  <p align="center">
2
- <h1 align="center">Universal ID 🌌</h1>
2
+ <h1 align="center">Universal ID</h1>
3
3
  <p align="center">
4
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" />
5
+ <img alt="Lines of Code" src="https://img.shields.io/badge/loc-692-47d299.svg" />
6
6
  </a>
7
7
  <a href="https://codeclimate.com/github/hopsoft/universalid/maintainability">
8
- <img src="https://api.codeclimate.com/v1/badges/80bcd3acced072534a3a/maintainability" />
8
+ <img src="https://api.codeclimate.com/v1/badges/567624cbe733fafc2330/maintainability" />
9
9
  </a>
10
10
  <a href="https://rubygems.org/gems/universalid">
11
11
  <img alt="GEM Version" src="https://img.shields.io/gem/v/universalid?color=168AFE&include_prereleases&logo=ruby&logoColor=FE1616">
@@ -16,6 +16,9 @@
16
16
  <a href="https://github.com/testdouble/standard">
17
17
  <img alt="Ruby Style" src="https://img.shields.io/badge/style-standard-168AFE?logo=ruby&logoColor=FE1616" />
18
18
  </a>
19
+ <a href="https://gitpod.io/#https://github.com/hopsoft/universalid">
20
+ <img alt="Gitpod - Ready to Code" src="https://img.shields.io/badge/Gitpod-Ready--to--Code-green?style=flat&logo=gitpod&logoColor=white" />
21
+ </a>
19
22
  <a href="https://github.com/hopsoft/universalid/actions/workflows/tests.yml">
20
23
  <img alt="Tests" src="https://github.com/hopsoft/universalid/actions/workflows/tests.yml/badge.svg" />
21
24
  </a>
@@ -30,248 +33,986 @@
30
33
  <img alt="Twitter Follow" src="https://img.shields.io/twitter/url?label=%40hopsoft&style=social&url=https%3A%2F%2Ftwitter.com%2Fhopsoft">
31
34
  </a>
32
35
  </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>
36
+ <h2 align="center">URL-Safe Portability for Ruby Objects</h2>
35
37
  </p>
36
38
 
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. 🤯
39
+ **Universal ID introduces a paradigm shift in Ruby development with powerful recursive serialization.**
40
+ This innovative library transforms any Ruby object into a URL-safe string, enabling efficient encoding and seamless data transfer across process boundaries. By simplifying complex serialization tasks, Universal ID enhances both the developer and end-user experience, paving the way for a wide range of use cases—from state preservation in web apps to inter-service communication.
40
41
 
41
- ## Sponsors
42
+ It leverages both [MessagePack](https://msgpack.org/) and [Brotli](https://github.com/google/brotli) _(a combo built for speed and best-in-class data compression)_.
43
+ MessagePack + Brotli is up to 30% faster and within 2-5% compression rates compared to Protobuf. <a title="Source" href="https://g.co/bard/share/e5bdb17aee91">↗</a>
42
44
 
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>
45
+ ## Example Use Cases
46
+
47
+ - **State Preservation in Web Apps**: Maintain the state of a user's session in web applications without storing data server-side
48
+ - **API Data Transfer**: Serialize complex data structures into a URI format for easy and efficient transfer via RESTful APIs
49
+ - **Bookmarkable Configurations**: Allow users to bookmark configurations of a web application by embedding the state in the URL
50
+ - **Deep Linking in Web Apps**: Create deep links that carry complex state information, allowing users to return to a specific state within an application
51
+ - **Debugging Tools**: Serialize objects and their state for logging purposes, aiding in debugging and error tracking
52
+ - **Shareable Reports and Views**: Encode the state of reports or customized views in web applications, making them shareable
53
+ - **Inter-Service Communication**: Facilitate communication between different services by passing complex objects in a standardized, URL-safe format
54
+ - **Client-Side Storage Optimization**: Reduce the need for client-side storage by keeping serialized state in URLs or Cookies
55
+ - **Versioning Serialized Objects**: Enable versioning of serialized objects in URLs, allowing users to access different states or versions of data
56
+ - **Data Export/Import**: Simplify the export and import process of complex objects between different environments or systems by using URI-encoded data
57
+
58
+ This is just a fraction of what's possible with Universal ID. It's an invaluable tool for a range of development needs. API design, data management, user experience, and more. **Endless possibilities!**
51
59
 
52
60
  <!-- Tocer[start]: Auto-generated, don't remove. -->
53
61
 
54
62
  ## Table of Contents
55
63
 
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)
64
+ - [Supported Data Types](#supported-data-types)
65
+ - [Scalars](#scalars)
66
+ - [Composites](#composites)
67
+ - [ActiveRecord](#activerecord)
68
+ - [Why Universal ID with ActiveRecord?](#why-universal-id-with-activerecord)
69
+ - [Custom Datatypes](#custom-datatypes)
70
+ - [Settings and Prepack Options](#settings-and-prepack-options)
71
+ - [Advanced ActiveRecord](#advanced-activerecord)
72
+ - [ActiveRecord::Relation Support](#activerecordrelation-support)
73
+ - [SignedGlobalID](#signedglobalid)
74
+ - [Performance and Benchmarks](#performance-and-benchmarks)
75
+ - [Sponsors](#sponsors)
66
76
  - [License](#license)
67
77
 
68
78
  <!-- Tocer[finish]: Auto-generated, don't remove. -->
69
79
 
70
- ## What is Global ID?
80
+ > :rocket: **Ready to Dive In?**: All the code examples below can be tested on your local machine. Simply clone the repo and run `bin/console` to begin exploring. Don't forget to execute `bundle` first to ensure all dependencies are up to date. Happy coding!
71
81
 
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
82
 
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.
83
+ ## Supported Data Types
79
84
 
80
- ### Global ID Examples
85
+ ### Scalars
81
86
 
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
- ```
87
+ Universal ID supports most Ruby primitives.
88
+
89
+ - `NilClass`
90
+ - `Complex`
91
+ - `Date`
92
+ - `DateTime`
93
+ - `FalseClass`
94
+ - `Float`
95
+ - `Integer`
96
+ - `NilClass`
97
+ - `Range`
98
+ - `Rational`
99
+ - `Regexp`
100
+ - `String`
101
+ - `Symbol`
102
+ - `Time`
103
+ - `TrueClass`
104
+
105
+ You can use Universal ID for individual primitives if desired, but scalar support is really the foundation for more serious use cases.
106
+ _See below..._
93
107
 
94
108
  ```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...
109
+ uri = URI::UID.build(:demo).to_s
110
+ #=> "uid://universal-id/iwKA1gBkZW1vAw"
111
+
112
+ uid = URI::UID.parse(uri)
113
+ #=> #<URI::UID uid://universal-id/iwKA1gBkZW1vAw>
99
114
 
100
- sgid = SignedGlobalID.parse("BAh7CEkiCGdpZAY6BkVUSSIhZ2lkOi8vVW...") # #<SignedGlobalID:0x000... @expires_at=nil, @purpose="...", @uri=#<URI::GID gid://UniversalID/Campaign/1>
101
- campaign == sgid.find # true
115
+ uid.decode
116
+ #=> :demo
102
117
  ```
103
118
 
104
- ## What is Universal ID?
119
+ ### Composites
105
120
 
106
- UniversalID extends GlobalID functionality to more objects.
121
+ Composite support is where things start to get interesting. All of the composite datatypes listed below can be recursively transformed into a Universal ID.
107
122
 
108
- - Array
109
- - Hash
110
- - ActiveModel _(unsaved)_
111
- - ActiveRecord::Relation
112
- - etc.
123
+ <details>
124
+ <summary><b><code>[]</code> Array</b>... ▾</summary>
125
+ <p></p>
113
126
 
114
- ### Why Expand Global ID?
127
+ ```ruby
128
+ array = [1, 2, 3, [:a, :b, :c, [true]]]
115
129
 
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.
130
+ uri = URI::UID.build(array).to_s
131
+ #=> "uid://universal-id/iweAlAECA5TUAGHUAGLUAGORwwM"
118
132
 
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? 😱
133
+ uid = URI::UID.parse(uri)
134
+ #=> #<URI::UID uid://universal-id/iweAlAECA5TUAGHUAGLUAGORwwM>
125
135
 
126
- **Don't fret!** UniversalID supports safely marshaling unsaved ActiveModels between steps.
136
+ uid.decode
137
+ #=> [1, 2, 3, [:a, :b, :c, [true]]]
138
+ ```
139
+ </details>
127
140
 
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.)
141
+ <details>
142
+ <summary><b><code>{}</code> Hash</b>... ▾</summary>
143
+ <p></p>
132
144
 
133
- # HTTP request / crossing a process boundary / etc.
145
+ ```ruby
146
+ hash = {a: 1, b: 2, c: 3, array: [1, 2, 3, [:a, :b, :c, [true]]]}
134
147
 
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
148
+ uri = URI::UID.build(hash).to_s
149
+ #=> "uid://universal-id/CxKAhNQAYQHUAGIC1ABjA8cFAGFycmF5lAEC..."
139
150
 
140
- # HTTP request / crossing a process boundary / etc.
151
+ uid = URI::UID.parse(uri)
152
+ #=> #<URI::UID uid://universal-id/CxKAhNQAYQHUAGIC1ABjA8cFAGFycmF5lAECA5TUAGHUAGLUAGORwwM>
141
153
 
142
- # Step 3-N. Continue multi-step form (enrich partial data)
143
- # ...
154
+ uid.decode
155
+ #=> {:a=>1, :b=>2, :c=>3, :array=>[1, 2, 3, [:a, :b, :c, [true]]]}
156
+ ```
157
+ </details>
144
158
 
145
- # Final Step. Save the data
146
- campaign = Campaign.new_from_portable_hash(param)
147
- campaign.save!
148
- ```
159
+ <details>
160
+ <summary><b><code><></code> Open Struct</b>... ▾</summary>
161
+ <p></p>
149
162
 
150
- **And... this is just one use-case!**
163
+ ```ruby
164
+ ostruct = OpenStruct.new(
165
+ name: "Wireless Keyboard",
166
+ price: 49.99,
167
+ category: "Electronics",
168
+ in_stock: true
169
+ )
151
170
 
152
- UniversalID can be used to solve a multitude of problems with minimal effort.
171
+ uri = URI::UID.build(ostruct).to_s
172
+ #=> "uid://universal-id/iyaAx0sMhNYAbmFtZbFXaXJlbGVzcyBLZXlib2FyZMcFAHByaWNly0BI_rhR64Uf1wBjYXRlZ29ye..."
153
173
 
154
- - Sharable artifacts (reports, configurations, etc.)
155
- - Data versioning
156
- - Digital products
157
- - etc. *the only limit is your imagination*
174
+ uid = URI::UID.parse(uri)
175
+ #=> #<URI::UID scheme=uid, host=universal-id, payload=iyaAx0sMhNYAbmFtZbFXaXJlbGVzcyBLZXlib2FyZMcFAHByaWNly0BI_rhR64Uf1wBjYXRlZ29ye...>
158
176
 
159
- ### Summary of Benefits
177
+ uid.decode
178
+ #=> #<OpenStruct name="Wireless Keyboard", price=49.99, category="Electronics", in_stock=true>
179
+ ```
180
+ </details>
160
181
 
161
- #### GlobalID
182
+ <details>
183
+ <summary><b><code>()</code> Set</b>... ▾</summary>
184
+ <p></p>
162
185
 
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
186
+ ```ruby
187
+ set = Set.new([1, 2, 3, [:a, :b, :c, [true]]])
170
188
 
171
- #### SignedGlobalID
189
+ uri = URI::UID.build(set).to_s
190
+ #=> "uid://universal-id/iwiA2AuUAQIDlNQAYdQAYtQAY5HDAw"
172
191
 
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")_
192
+ uid = URI::UID.parse(uri)
193
+ #=> #<URI::UID uid://universal-id/iwiA2AuUAQIDlNQAYdQAYtQAY5HDAw>
178
194
 
179
- ### Hash
195
+ URI::UID.parse(uri).decode
196
+ #=> #<Set: {1, 2, 3, [:a, :b, :c, [true]]}>
197
+ ```
198
+ </details>
180
199
 
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...
200
+ <details>
201
+ <summary><b><code><></code> Struct</b>... ▾</summary>
202
+ <p></p>
186
203
 
204
+ ```ruby
205
+ Book = Struct.new(:title, :author, :isbn, :published_year)
206
+ book = Book.new("The Great Gatsby", "F. Scott Fitzgerald", "9780743273565", 1925)
187
207
 
188
- UniversalID::PortableHash.parse_gid(gid_param).find
189
- {"name"=>"Example", "list"=>[1, 2, 3], "object"=>{"nested"=>true}}
208
+ uri = URI::UID.build(book).to_s
209
+ #=> "uid://universal-id/G2YAoGTomv9N_4RV2oJRxRvZdC1wNJ0H3Ipu45kVcSrAxtg6Wjtogpi6GV1XXQAOAXoNR3BrCg9AQ..."
190
210
 
211
+ uid = URI::UID.parse(uri)
212
+ #=> #<URI::UID scheme=uid, host=universal-id, payload=G2YAoGTomv9N_4RV2oJRxRvZdC1wNJ0H3Ipu45kVcSrAxtg6Wjtogpi6GV1XXQAOAXoNR3BrCg9AQ...>
191
213
 
192
- UniversalID::PortableHash.parse_gid(sgid_param).find
193
- {"name"=>"Example", "list"=>[1, 2, 3], "object"=>{"nested"=>true}}
194
- ```
214
+ uid.decode
215
+ #=> #<struct Book title="The Great Gatsby", author="F. Scott Fitzgerald", isbn="9780743273565", published_year=1925>
216
+ ```
217
+ </details>
195
218
 
196
- ### ActiveModel
219
+ ### ActiveRecord
197
220
 
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...
221
+ > :information_source: **Broad Compatibility**: Universal ID has built-in support for ActiveRecord, yet it maintains independence from Rails-specific dependencies. This versatile design enables integration into **any Ruby project**.
202
222
 
203
- copy = Email.new_from_portable_hash(gid)
204
- signed_copy = Email.new_from_portable_hash(sgid)
223
+ #### Why Universal ID with ActiveRecord?
205
224
 
206
- email.save!
225
+ While ActiveRecord already supports GlobalID, a robust library for serializing individual ActiveRecord models, Universal ID extends this functionality to cover a wider range of use cases. Here are a few reasons you may want to consider Universal ID.
207
226
 
208
- options = {portable_hash_options: {except: [:id, :created_at, :updated_at]}}
209
- # NOTE: Options can be configured globally via UniversalID.config.portable_hash
227
+ - **Support for New Records**: Unlike GlobalID, Universal ID can serialize models that haven't been saved to the database yet
228
+ - **Capturing Unsaved Changes**: It can serialize ActiveRecord models with unsaved changes, ensuring that even transient states are captured
229
+ - **Association Handling**: Universal ID goes beyond single models. It can serialize associated records, including those with unsaved changes, creating a comprehensive snapshot of complex object states
230
+ - **Cloning Existing Records**: Need to make a copy of a record, including its associations? Universal ID handles this effortlessly, making it ideal for duplicating complex datasets
231
+ - **Granular Data Control**: With Universal ID, you gain explicit control over the serialization process. You can precisely choose which columns to include or exclude, allowing for tailored, optimized payloads that fit your specific needs
232
+ - **Efficient Query Serialization**: Universal ID extends its capabilities to ActiveRecord relations, enabling the serialization of complex queries and scopes. This feature allows for seamless sharing of query logic between processes, ensuring consistency and reducing redundancy in data handling tasks.
210
233
 
211
- gid = email.to_portable_hash_gid_param(options)
212
- sgid = email.to_portable_hash_gid_param(options)
234
+ In summary, while GlobalID excels in its specific use case, Universal ID offers extended capabilities, particularly useful in scenarios involving unsaved records, complex associations, and data cloning.
213
235
 
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
- ```
236
+ <details>
237
+ <summary><b>How to Convert Records to UIDs</b>... ▾</summary>
238
+ <p></p>
218
239
 
219
- ### Benchmarks
240
+ ```ruby
241
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
220
242
 
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
- ```
243
+ ActiveRecord::Schema.define do
244
+ create_table :campaigns do |t|
245
+ t.column :name, :string
246
+ t.timestamps
247
+ end
248
+ end
249
+
250
+ class Campaign < ApplicationRecord
251
+ end
252
+
253
+ # ---
254
+
255
+ campaign = Campaign.create(name: "Marketing Campaign")
256
+
257
+ uri = URI::UID.build(campaign).to_s
258
+ #=> "uid://universal-id/CwiAxw4EqENhbXBhaWdugaJpZAMD"
259
+
260
+ uid = URI::UID.parse(uri)
261
+ #=> #<URI::UID uid://universal-id/CwiAxw4EqENhbXBhaWdugaJpZAMD>
262
+
263
+ URI::UID.parse(uri).decode
264
+ ##<Campaign:0x000000011cc67da8 id: 1, name: "Marketing Campaign", ...>
265
+ ```
266
+ </details>
267
+
268
+ ### Custom Datatypes
269
+
270
+ Universal ID is **extensible** so you can register your own datatypes with specialized serialization rules.
271
+ It couldn't be simpler. Just convert the required data to a Ruby scalar or composite value.
272
+
273
+ <details>
274
+ <summary><b>How to Register your own Datatype</b>... ▾</summary>
275
+ <p></p>
276
+
277
+ ```ruby
278
+ class UserSettings
279
+ attr_accessor :user_id, :preferences
280
+
281
+ def initialize(user_id, preferences = {})
282
+ @user_id = user_id
283
+ @preferences = preferences
284
+ end
285
+ end
286
+
287
+ UniversalID::MessagePackFactory.register(
288
+ type: UserSettings,
289
+ packer: ->(user_preferences, packer) do
290
+ packer.write user_preferences.user_id
291
+ packer.write user_preferences.preferences
292
+ end,
293
+ unpacker: ->(unpacker) do
294
+ user_id = unpacker.read
295
+ preferences = unpacker.read
296
+ UserSettings.new user_id, preferences
297
+ end
298
+ )
299
+
300
+ settings = UserSettings.new(1,
301
+ theme: "dark",
302
+ notifications: "email",
303
+ language: "en",
304
+ layout: "grid",
305
+ privacy: "private"
306
+ )
307
+
308
+ uri = URI::UID.build(settings).to_s
309
+ #=> "uid://universal-id/G1QAQAT-bfcGW1QOgadJwJF06yL8gDnGgfs1Xdti20TDDvG5STPqzbYcQ6TBqVKhdZ39CdQZUwEGe..."
310
+
311
+ uid = URI::UID.parse(uri)
312
+ #=> #<URI::UID uid://universal-id/G1QAQAT-bfcGW1QOgadJwJF06yL8gDnGgfs1Xdti20TDDvG5STPqzbYcQ6TBqVKhdZ39CdQZUwEGe..."
313
+
314
+ uid.decode
315
+ => #<UserSettings:0x0000000139157dd8 @preferences={:theme=>"dark", :notifications=>"email", :language=>"en", :layout=>"grid", :privacy=>"private"}, @user_id=1>
316
+ ```
317
+ </details>
318
+
319
+ ## Settings and Prepack Options
320
+
321
+ Universal ID supports a small but powerful set of configuration options for transforming objects before being
322
+ handed off to MessagePack for serialization.
323
+
324
+ Prepacking gives you explicit control over what data to include in the Universal ID.
325
+
326
+
327
+ <details>
328
+ <summary><b>View All Settings and Prepack Options</b>... ▾</summary>
329
+ <p></p>
330
+
331
+ ```yml
332
+ prepack:
333
+ # ..........................................................................................................
334
+ # A list of attributes to exclude (for objects like Hash, OpenStruct, Struct, etc.)
335
+ # Takes prescedence over the`include` list
336
+ exclude: []
337
+
338
+ # ..........................................................................................................
339
+ # A list of attributes to include (for objects like Hash, OpenStruct, Struct, etc.)
340
+ include: []
341
+
342
+ # ..........................................................................................................
343
+ # Whether or not to omit blank values when packing (nil, {}, [], "", etc.)
344
+ include_blank: true
345
+
346
+ # ==========================================================================================================
347
+ # Database records
348
+ database:
349
+ # ......................................................................................................
350
+ # Whether or not to include primary/foreign keys
351
+ # Setting this to `false` can be used to make a copy of an existing record
352
+ include_keys: true
353
+
354
+ # ......................................................................................................
355
+ # Whether or not to include date/time timestamps (created_at, updated_at, etc.)
356
+ # Setting this to `false` can be used to make a copy of an existing record
357
+ include_timestamps: true
358
+
359
+ # ......................................................................................................
360
+ # Whether or not to include unsaved changes
361
+ # Assign to `true` when packing new records
362
+ include_unsaved_changes: false
363
+
364
+ # ......................................................................................................
365
+ # Whether or not to include loaded in-memory descendants (i.e. child associations)
366
+ include_descendants: false
367
+
368
+ # ......................................................................................................
369
+ # The max depth (number) of loaded in-memory descendants to include when `include_descendants == true`
370
+ # For example, a value of (3) would include the following:
371
+ # Parent > Child > Grandchild
372
+ descendant_depth: 0
373
+ ```
374
+ </details>
375
+
376
+ Prepack options can be applied when creating a Universal ID and can be passed in structured or flat format.
377
+
378
+ <details>
379
+ <summary><b>How to Apply Prepack Options when Creating UIDs</b>... ▾</summary>
380
+ <p></p>
381
+
382
+ ```ruby
383
+ person = {
384
+ full_name: "Jane Doe",
385
+ email: "janedoe@example.com",
386
+ birthdate: "1980-05-15",
387
+ phone_number: "555-6789",
388
+ ssn: "123-45-6789",
389
+ children: [
390
+ {
391
+ full_name: "Alice Doe",
392
+ email: "alicedoe@example.com",
393
+ birthdate: nil,
394
+ phone_number: "555-1234",
395
+ ssn: "987-65-4321"
396
+ },
397
+ {
398
+ full_name: "Bob Doe",
399
+ email: "bobdoe@example.com",
400
+ birthdate: "2008-11-21",
401
+ phone_number: nil,
402
+ ssn: "456-12-1234"
403
+ }
404
+ ]
405
+ }
406
+
407
+ uid = URI::UID.build(person, include_blank: false, exclude: [:phone_number, :ssn])
408
+ uid.decode
409
+
410
+ # Note that the decoded payload is smaller due to the prepack options
411
+ # Also note that the options were applied recursively
412
+
413
+ {
414
+ full_name: "Jane Doe",
415
+ email: "janedoe@example.com",
416
+ birthdate: "1980-05-15",
417
+ children: [
418
+ {
419
+ full_name: "Alice Doe",
420
+ email: "alicedoe@example.com"
421
+ },
422
+ {
423
+ full_name: "Bob Doe",
424
+ email: "bobdoe@example.com",
425
+ birthdate: "2008-11-21"
426
+ }
427
+ ]
428
+ }
429
+ ```
430
+ </details>
431
+
432
+ It's also possible to register frequently used options as reusable settings to further simplify creating UIDs.
433
+
434
+ <details>
435
+ <summary><b>How to Register Prepack Options as Preconfigured Settings</b>... ▾</summary>
436
+ <p></p>
437
+
438
+ ```yaml
439
+ # app/config/unsaved.yml
440
+ prepack:
441
+ include_blank: false
442
+
443
+ database:
444
+ include_unsaved_changes: true
445
+ include_timestamps: false
446
+ ```
447
+
448
+ ```ruby
449
+ UniversalID::Settings.register :unsaved, YAML.safe_load("app/config/unsaved.yml")
450
+ URI::UID.build @record, UniversalID::Settings[:small_record]
451
+ ```
452
+ </details>
453
+
454
+ ## Advanced ActiveRecord
455
+
456
+ Universal ID includes some advanced capabilities when used with ActiveRecord.
457
+
458
+ - [x] **Include loaded associations**
459
+ Universal ID supports including `loaded` associations when a model is transformed into a UID.
460
+ <small><em>Note that associations must be `loaded?` to be considered candidates for inclusion. There are multiple ways to achieve this, so be sure to [read up on associations](https://guides.rubyonrails.org/association_basics.html).</em></small>
461
+
462
+ - [x] **Include unsaved changes**
463
+ Universal ID supports capturing unsaved change, for both new and persisted records, when a model is transformed into a UID.
464
+ <small><em>This allows you to marshal complex unsaved data that can be restored at a later time. This feature supports several use cases, like allowing users to pause their work and resume at any point in the future without the need to store partial records in your database. And, because UIDs are web safe, you can hold this data in URLs, browser Cookies, Local/SessionStorage, etc.</em></small>
465
+
466
+ - [x] **Exclude keys** to make copies of existing records
467
+ Universal ID supports making copies of individual records or entire collections by opt'ing to exclude keys when transorming to UID.
468
+ <small><em>This allows you to make data sharable. Consider a sencario with complex infrastructure (db sharding, etc.). You can leverage Universal ID to move entire subsets of data across physical data stores.</em></small>
469
+
470
+ First, let's establish the schema structure and data we'll be working with.
471
+ We'll limit ourselves to 3 tables here, but Universal ID can support much more complex data models.
472
+
473
+ - Campaign
474
+ - Email
475
+ - Attachment
476
+
477
+ We'll use 1 campaign with 3 emails that have 2 attachments each.
478
+
479
+ <details>
480
+ <summary><b>Setup the Schema</b>... ▾</summary>
481
+ <p></p>
482
+
483
+ ```ruby
484
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
485
+
486
+ ActiveRecord::Schema.define do
487
+ create_table :campaigns do |t|
488
+ t.column :name, :string
489
+ t.column :description, :text
490
+ t.column :trigger, :string
491
+ t.timestamps
492
+ end
493
+
494
+ create_table :emails do |t|
495
+ t.column :campaign_id, :integer
496
+ t.column :subject, :string
497
+ t.column :body, :text
498
+ t.column :wait, :integer
499
+ t.timestamps
500
+ end
501
+
502
+ create_table :attachments do |t|
503
+ t.column :email_id, :integer
504
+ t.column :file_name, :string
505
+ t.column :content_type, :string
506
+ t.column :file_size, :integer
507
+ t.column :file_data, :binary
508
+ t.timestamps
509
+ end
510
+ end
511
+ ```
512
+ </details>
513
+
514
+ <details>
515
+ <summary><b>Setup the Models</b>... ▾</summary>
516
+ <p></p>
517
+
518
+ ```ruby
519
+ class Campaign < ApplicationRecord
520
+ has_many :emails, dependent: :destroy
521
+ end
522
+
523
+ class Email < ApplicationRecord
524
+ belongs_to :campaign
525
+ has_many :attachments, dependent: :destroy
526
+ end
527
+
528
+ class Attachment < ApplicationRecord
529
+ belongs_to :email
530
+ end
531
+ ```
532
+ </details>
533
+
534
+
535
+ <details>
536
+ <summary><b>Setup the Model Instances</b>... ▾</summary>
537
+ <p></p>
538
+
539
+ ```ruby
540
+ campaign = Campaign.new(
541
+ name: "Summer Sale Campaign",
542
+ description: "A campaign for the summer sale, targeting our loyal customers.",
543
+ trigger: "SummerStart"
544
+ )
545
+
546
+ # NOTE: Assigning campaign.emails via `=` to ensure ActiveRecord flags the association as `loaded`
547
+ campaign.emails = 3.times.map do |i|
548
+ email = campaign.emails.build(
549
+ subject: "Summer Sale Special Offer #{i + 1}",
550
+ body: "Dear Customer, check out our exclusive summer sale offers! #{i + 1}",
551
+ wait: rand(1..14)
552
+ )
553
+
554
+ # NOTE: Assigning email.attachments via `=` to ensure ActiveRecord flags the association as `loaded`
555
+ email.tap do |e|
556
+ e.attachments = 2.times.map do |j|
557
+ data = SecureRandom.random_bytes(rand(500..1500))
558
+ e.attachments.build(
559
+ file_name: "summer_sale_#{i + 1}_attachment_#{j + 1}.pdf",
560
+ content_type: "application/pdf",
561
+ file_size: data.size,
562
+ file_data: data
563
+ )
564
+ end
565
+ end
566
+ end
567
+
568
+ # demonstrate that we have new unsaved records
569
+
570
+ #campaign
571
+ campaign.new_record? # true
572
+ campaign.changed? # true
573
+
574
+ # emails
575
+ campaign.emails.each do |email|
576
+ email.new_record? # true
577
+ email.changed? # true
578
+
579
+ email.attachments.each do |attachment|
580
+ attachment.new_record? # true
581
+ attachment.changed? # true
582
+ end
583
+ end
584
+ ```
585
+ </details>
586
+
587
+ Now let's look at how to leverage Universal ID with ActiveRecord.
588
+
589
+ <details>
590
+ <summary><b>How to Include Unsaved Changes for New Records</b>... ▾</summary>
591
+ <p></p>
592
+
593
+ ```ruby
594
+ # prepack options
595
+ options = {
596
+ include_unsaved_changes: true,
597
+ include_descendants: true,
598
+ descendant_depth: 2
599
+ }
600
+
601
+ # NOTE: The campaign model instance was setup earlier in the "Model Instances" section above
602
+ campaign.new_record? # true
603
+ campaign.changes
604
+ # {
605
+ # "name"=>[nil, "Summer Sale Campaign"],
606
+ # "description"=>[nil, "A campaign for the summer sale, targeting our loyal customers."],
607
+ # "trigger"=>[nil, "SummerStart"]
608
+ # }
609
+
610
+ campaign.emails.each do |email|
611
+ email.new_record? # true
612
+ email.changes
613
+ # {
614
+ # "subject"=>[nil, "Summer Sale Special Offer ..."],
615
+ # "body"=>[nil, "Dear Customer, check out our exclusive summer sale offers! ..."],
616
+ # "wait"=>[nil, ...]
617
+ # }
618
+
619
+ email.attachments.each do |attachment|
620
+ attachment.new_record? # true
621
+ attachment.changes
622
+ # {
623
+ # "file_name"=>[nil, "summer_sale_..._attachment_....pdf"],
624
+ # "content_type"=>[nil, "application/pdf"],
625
+ # "file_size"=>[nil, ...],
626
+ # "file_data"=>[nil, "..."]
627
+ # }
628
+ end
629
+ end
630
+
631
+ encoded = URI::UID.build(campaign, options).to_s
632
+ restored = URI::UID.parse(encoded).decode
633
+
634
+ restored.new_record? # true
635
+ restored.changes
636
+ # {
637
+ # "name"=>[nil, "Summer Sale Campaign"],
638
+ # "description"=>[nil, "A campaign for the summer sale, targeting our loyal customers."],
639
+ # "trigger"=>[nil, "SummerStart"]
640
+ # }
641
+
642
+ restored.emails.each do |email|
643
+ email.new_record? # true
644
+ email.changes
645
+ # {
646
+ # "subject"=>[nil, "Summer Sale Special Offer ..."],
647
+ # "body"=>[nil, "Dear Customer, check out our exclusive summer sale offers! ..."],
648
+ # "wait"=>[nil, ...]
649
+ # }
650
+
651
+ email.attachments.each do |attachment|
652
+ attachment.new_record? # true
653
+ attachment.changes
654
+ # {
655
+ # "file_name"=>[nil, "summer_sale_..._attachment_....pdf"],
656
+ # "content_type"=>[nil, "application/pdf"],
657
+ # "file_size"=>[nil, ...],
658
+ # "file_data"=>[nil, "..."]
659
+ # }
660
+ end
661
+ end
662
+ ```
663
+ </details>
664
+
665
+ <details>
666
+ <summary><b>How to Include Unsaved Changes for Persisted Records</b>... ▾</summary>
667
+ <p></p>
668
+
669
+ ```ruby
670
+ # NOTE: The campaign model instance was setup earlier in the "Model Instances" section above
671
+ # persist the model and its associations
672
+ campaign.save!
673
+
674
+ # make some unsaved changes to the records
675
+ campaign.name = "Changed Name #{SecureRandom.hex}"
676
+ campaign.emails.each do |email|
677
+ email.subject = "Changed Subject #{SecureRandom.hex}"
678
+ email.attachments.each do |attachment|
679
+ attachment.file_name = "changed_file_name#{SecureRandom.hex}.pdf"
680
+ end
681
+ end
682
+
683
+ campaign.persisted? # true
684
+ campaign.changes
685
+ # {"name"=>["Summer Sale Campaign", "Changed Name ..."]}
686
+
687
+ campaign.emails.each do |email|
688
+ email.persisted? # true
689
+ email.changes
690
+ # {"subject"=>["Summer Sale Special Offer 1", "Changed Subject ..."]}
691
+
692
+ email.attachments.each do |attachment|
693
+ attachment.persisted? # true
694
+ attachment.changes
695
+ # {"file_name"=>["summer_sale_..._attachment_....pdf", "changed_file_name....pdf"]}
696
+ end
697
+ end
698
+
699
+ # prepack options
700
+ options = {
701
+ include_unsaved_changes: true,
702
+ include_descendants: true,
703
+ descendant_depth: 2
704
+ }
705
+
706
+ encoded = URI::UID.build(campaign, options).to_s
707
+ restored = URI::UID.parse(encoded).decode
708
+
709
+ restored.persisted? # true
710
+ restored.changes
711
+ # {"name"=>["Summer Sale Campaign", "Changed Name ..."]}
712
+
713
+ restored.emails.each do |email|
714
+ email.persisted? # true
715
+ email.changes
716
+ # {"subject"=>["Summer Sale Special Offer 1", "Changed Subject ..."]}
717
+
718
+ email.attachments.each do |attachment|
719
+ attachment.persisted? # true
720
+ attachment.changes
721
+ # {"file_name"=>["summer_sale_..._attachment_....pdf", "changed_file_name....pdf"]}
722
+ end
723
+ end
724
+ ```
725
+ </details>
726
+
727
+ <details>
728
+ <summary><b>How to Copy Persisted Records</b>... ▾</summary>
729
+ <p></p>
730
+
731
+ ```ruby
732
+ # NOTE: The campaign model instance was setup earlier in the "Model Instances" section above
733
+ # persist the model and its associations
734
+ campaign.save!
735
+
736
+ options = {
737
+ include_keys: false,
738
+ include_timestamps: false,
739
+ include_unsaved_changes: true,
740
+ include_descendants: true,
741
+ descendant_depth: 2
742
+ }
743
+
744
+ encoded = URI::UID.build(campaign, options).to_s
745
+ copy = URI::UID.parse(encoded).decode
746
+
747
+ campaign.persisted? # false
748
+ copy.new_record? # true
749
+ copy.save!
750
+
751
+ copy == campaign # false
752
+
753
+ campaign.emails.each do |email|
754
+ copy_email_ids = copy.emails.map(&:id)
755
+ campaign_email_ids = campaign.emails.map(&:id)
756
+ (copy_email_ids && campaign_email_ids).any? # false
757
+
758
+ copy_attachment_ids = copy.emails.map(&:attachments).flatten.map(&:id)
759
+ campaign_attachment_ids = campaign.emails.map(&:attachments).flatten.map(&:id)
760
+ (copy_attachment_ids & campaign_attachment_ids).any? # false
761
+ end
762
+ ```
763
+ </details>
764
+
765
+ ## ActiveRecord::Relation Support
766
+
767
+ Universal ID seamlessly handles the serialization of ActiveRecord relations and scopes, striking the perfect balance between efficiency and functionality. It paves the way for easy, optimized, and effective sharing of database queries. This capability transforms query management, allowing developers to encapsulate complex query structures into a reliable, portable, and reusable format that ensures query consistency across different parts of the application.
768
+
769
+ > :bulb: **Optimized Payloads**: When handling `ActiveRecord::Relations`, Universal ID intelligently clears cached data within the relation before encoding. This approach minimizes payload size, ensuring efficient data transfer without sacrificing the integrity of the original query logic.
770
+
771
+ <details>
772
+ <summary><b>How to work with ActiveRecord::Relations</b>... ▾</summary>
773
+ <p></p>
774
+
775
+ ```ruby
776
+ # Assuming we have multiple campaigns already stored in the database
777
+ relation = Campaign.joins(:emails).where("emails.subject LIKE ?", "Flash Sale%")
778
+
779
+ # force load the relation
780
+ relation.load
781
+ relation.loaded? # true
782
+
783
+ encoded = URI::UID.build(relation).to_s
784
+ decoded = URI::UID.parse(encoded).decode
785
+
786
+ decoded.is_a? ActiveRecord::Relation # true
787
+ decoded.loaded? # false
788
+ decoded == relation # true
789
+ decoded.size == relation.size # true
790
+ decoded.to_a == relation.to_a # true
791
+ ```
792
+ </details>
793
+
794
+ ## SignedGlobalID
795
+
796
+ Features like `signing` _(to prevent tampering)_, `purpose`, and `expiration` are provided by SignedGlobalIDs.
797
+ These features _(and more)_ will eventually be added to UniversalID, but until then...
798
+ simply convert your UniversalID to a SignedGlobalID to add these features to any Universal ID.
799
+
800
+ <details>
801
+ <summary><b>How to Convert a UID to/from a SignedGlobalID</b>... ▾</summary>
802
+ <p></p>
803
+
804
+ ```ruby
805
+ product = {
806
+ name: "Wireless Bluetooth Headphones",
807
+ price: 79.99,
808
+ category: "Electronics"
809
+ }
810
+
811
+ uid = URI::UID.build(product)
812
+ #=> #<URI::UID scheme="uid", host="universal-id", path="/G0sAgBypU587HsjkLpEnGHiaWfPQEyiiuH6j...">
813
+
814
+ sgid = uid.to_sgid_param(for: "cart-123", expires_in: 1.hour)
815
+ #=> "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJZ0d4WjJsa09pOHZkVzVwZG1WeWMyRnNMV2xrTDFWU1NUbzZWVWxFT2pwSGJHOWlZV3hKUkZKbFkyOXlaQzlITUhO..."
816
+
817
+ URI::UID.from_sgid(sgid, for: "cart-123").decode
818
+ #=> {
819
+ # name: "Wireless Bluetooth Headphones",
820
+ # price: 79.99,
821
+ # category: "Electronics"
822
+ # }
823
+
824
+
825
+ # mismatched purpose returns nil... as expected
826
+ URI::UID.from_sgid(sgid, for: "mismatch")
827
+ #=> nil
828
+ ```
829
+ </details>
830
+
831
+ ## Performance and Benchmarks
832
+
833
+ <details>
834
+ <summary><b>View Benchmarks</b>... ▾</summary>
835
+ <p></p>
836
+
837
+ Benchmarks can be performed by cloning the project and running `bin/bench`.
838
+ The run below was performed on the following hardware.
839
+
840
+ ```
841
+ Model Name: MacBook Air
842
+ Model Identifier: MacBookAir10,1
843
+ Chip: Apple M1
844
+ Total Number of Cores: 8 (4 performance and 4 efficiency)
845
+ Memory: 16 GB
846
+ ```
847
+
848
+ ```
849
+ Benchmarking with the following ActiveRecord/Hash data...
850
+ ==================================================================================================
851
+ {
852
+ "id" => 1,
853
+ "name" => "Production",
854
+ "description" => "RISC is good Well then get your shit together. Get it all together and put it in a backpack, all your shit, so it's together. ...and if you gotta take it somewhere, take it somewhere ya know? Take it to the shit store and sell it, or put it in a shit museum. I don't care what you do, you just gotta get it together... Get your shit together. Mark it zero! What you do not smell is called Iocane Power. You wanna hear a lie? ... I...think you're great. You're my best friend.",
855
+ "trigger" => "Political Organization enhance web-enabled architectures",
856
+ "created_at" => "2023-11-11T01:28:46.657Z",
857
+ "updated_at" => "2023-11-11T01:28:46.657Z",
858
+ "emails" => [
859
+ [0] {
860
+ "id" => 1,
861
+ "campaign_id" => 1,
862
+ "subject" => "drive synergistic web-readiness",
863
+ "body" => "But first things first. To the death! I feel like all my kids grew up, and then they married each other. It's every parents' dream. Koona t'chuta Solo? (Going somewhere Solo?)",
864
+ "wait" => nil,
865
+ "created_at" => "2023-11-11T01:28:46.661Z",
866
+ "updated_at" => "2023-11-11T01:28:46.675Z",
867
+ "attachments" => [
868
+ [0] {
869
+ "id" => 1,
870
+ "email_id" => 1,
871
+ "file_name" => "Schneider and Sons",
872
+ "content_type" => "Enterprise-wide 4th generation complexity",
873
+ "file_size" => nil,
874
+ "file_data" => nil,
875
+ "created_at" => "2023-11-11T01:28:46.664Z",
876
+ "updated_at" => "2023-11-11T01:28:46.670Z"
877
+ },
878
+ [1] {
879
+ "id" => 2,
880
+ "email_id" => 1,
881
+ "file_name" => "Devolved solution-oriented circuit",
882
+ "content_type" => "revolutionize magnetic bandwidth Intelligent Paper Gloves",
883
+ "file_size" => nil,
884
+ "file_data" => nil,
885
+ "created_at" => "2023-11-11T01:28:46.664Z",
886
+ "updated_at" => "2023-11-11T01:28:46.670Z"
887
+ }
888
+ ]
889
+ },
890
+ [1] {
891
+ "id" => 2,
892
+ "campaign_id" => 1,
893
+ "subject" => "Marketing",
894
+ "body" => "I'll explain and I'll use small words so that you'll be sure to understand, you warthog faced buffoon. Well then get your shit together. Get it all together and put it in a backpack, all your shit, so it's together. ...and if you gotta take it somewhere, take it somewhere ya know? Take it to the shit store and sell it, or put it in a shit museum. I don't care what you do, you just gotta get it together... Get your shit together. I am running away from my responsibilities. And it feels good.",
895
+ "wait" => nil,
896
+ "created_at" => "2023-11-11T01:28:46.671Z",
897
+ "updated_at" => "2023-11-11T01:28:46.675Z",
898
+ "attachments" => [
899
+ [0] {
900
+ "id" => 3,
901
+ "email_id" => 2,
902
+ "file_name" => "Weber-Schulist benchmark open-source applications",
903
+ "content_type" => "Enormous Linen Shoes synthesize customized e-services",
904
+ "file_size" => nil,
905
+ "file_data" => nil,
906
+ "created_at" => "2023-11-11T01:28:46.672Z",
907
+ "updated_at" => "2023-11-11T01:28:46.672Z"
908
+ },
909
+ [1] {
910
+ "id" => 4,
911
+ "email_id" => 2,
912
+ "file_name" => "thought leadership",
913
+ "content_type" => "Business Development Enhanced logistical collaboration",
914
+ "file_size" => nil,
915
+ "file_data" => nil,
916
+ "created_at" => "2023-11-11T01:28:46.672Z",
917
+ "updated_at" => "2023-11-11T01:28:46.672Z"
918
+ }
919
+ ]
920
+ },
921
+ [2] {
922
+ "id" => 3,
923
+ "campaign_id" => 1,
924
+ "subject" => "Mediocre Aluminum Car",
925
+ "body" => "Don’t even trip dawg. Stay away from my special lady friend, man.",
926
+ "wait" => nil,
927
+ "created_at" => "2023-11-11T01:28:46.672Z",
928
+ "updated_at" => "2023-11-11T01:28:46.675Z",
929
+ "attachments" => [
930
+ [0] {
931
+ "id" => 5,
932
+ "email_id" => 3,
933
+ "file_name" => "Import and Export",
934
+ "content_type" => "Heavy Duty Paper Bench Project Management",
935
+ "file_size" => nil,
936
+ "file_data" => nil,
937
+ "created_at" => "2023-11-11T01:28:46.673Z",
938
+ "updated_at" => "2023-11-11T01:28:46.674Z"
939
+ },
940
+ [1] {
941
+ "id" => 6,
942
+ "email_id" => 3,
943
+ "file_name" => "synthesize ubiquitous architectures Corporate Communications",
944
+ "content_type" => "Durable Rubber Watch",
945
+ "file_size" => nil,
946
+ "file_data" => nil,
947
+ "created_at" => "2023-11-11T01:28:46.674Z",
948
+ "updated_at" => "2023-11-11T01:28:46.674Z"
949
+ }
950
+ ]
951
+ }
952
+ ]
953
+ }
954
+ ==================================================================================================
955
+ Benchmarking 5000 iterations
956
+ ==================================================================================================
957
+ user system total real
958
+ URI::UID.build Hash 14.770667 0.102535 14.873202 ( 14.898856)
959
+ Average 0.002954 0.000021 0.002975 ( 0.002980)
960
+ ..................................................................................................
961
+ user system total real
962
+ URI::UID.build Hash, include_blank: false 13.821420 0.066910 13.888330 ( 13.892066)
963
+ Average 0.002764 0.000013 0.002778 ( 0.002778)
964
+ ..................................................................................................
965
+ user system total real
966
+ URI::UID.parse HASH/UID 0.075566 0.000411 0.075977 ( 0.076035)
967
+ Average 0.000015 0.000000 0.000015 ( 0.000015)
968
+ ..................................................................................................
969
+ user system total real
970
+ URI::UID.decode HASH/UID 0.111007 0.003572 0.114579 ( 0.114587)
971
+ Average 0.000022 0.000001 0.000023 ( 0.000023)
972
+ ..................................................................................................
973
+ user system total real
974
+ URI::UID.build ActiveRecord 0.984594 0.010059 0.994653 ( 0.994662)
975
+ Average 0.000197 0.000002 0.000199 ( 0.000199)
976
+ ..................................................................................................
977
+ user system total real
978
+ URI::UID.build ActiveRecord, exclude_blank 0.953653 0.006692 0.960345 ( 0.960765)
979
+ Average 0.000191 0.000001 0.000192 ( 0.000192)
980
+ ..................................................................................................
981
+ user system total real
982
+ URI::UID.build ActiveRecord, include_descendants 44.958468 0.170125 45.128593 ( 45.176116)
983
+ Average 0.008992 0.000034 0.009026 ( 0.009035)
984
+ ..................................................................................................
985
+ user system total real
986
+ URI::UID.parse ActiveRecord/UID 0.119030 0.000319 0.119349 ( 0.119525)
987
+ Average 0.000024 0.000000 0.000024 ( 0.000024)
988
+ ..................................................................................................
989
+ user system total real
990
+ URI::UID.decode HASH/UID 5.198092 0.024652 5.222744 ( 5.282794)
991
+ Average 0.001040 0.000005 0.001045 ( 0.001057)
992
+ ..................................................................................................
993
+ user system total real
994
+ UID > GID > UID.decode include_descendants 55.612061 0.398193 56.010254 ( 57.372350)
995
+ Average 0.011122 0.000080 0.011202 ( 0.011474)
996
+ ..................................................................................................
997
+ user system total real
998
+ UID > SGID > UID.decode include_descendants 55.406590 0.260552 55.667142 ( 56.432082)
999
+ Average 0.011081 0.000052 0.011133 ( 0.011286)
1000
+ ..................................................................................................
1001
+ ```
1002
+ </details>
1003
+
1004
+ ## Sponsors
1005
+
1006
+ <p align="center">
1007
+ <em>Proudly sponsored by</em>
1008
+ </p>
1009
+ <p align="center">
1010
+ <a href="https://www.clickfunnels.com?utm_source=hopsoft&utm_medium=open-source&utm_campaign=universalid">
1011
+ <img src="https://images.clickfunnel.com/uploads/digital_asset/file/176632/clickfunnels-dark-logo.svg" width="575" />
1012
+ </a>
1013
+ </p>
1014
+
1015
+ [Add your company...](https://github.com/sponsors/hopsoft/sponsorships?sponsor=hopsoft&tier_id=23918&preview=false)
275
1016
 
276
1017
  ## License
277
1018