universalid 0.0.1 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +959 -186
- data/Rakefile +1 -5
- data/config/default.yml +12 -0
- data/config/example.yml +45 -0
- data/lib/universal_id/contrib/active_record/base_message_pack_type.rb +11 -0
- data/lib/universal_id/contrib/active_record/base_packer.rb +130 -0
- data/lib/universal_id/contrib/active_record/base_unpacker.rb +52 -0
- data/lib/universal_id/contrib/active_record/relation_message_pack_type.rb +16 -0
- data/lib/universal_id/contrib/active_record.rb +4 -0
- data/lib/universal_id/contrib/active_support/time_with_zone_message_pack_type.rb +14 -0
- data/lib/universal_id/contrib/active_support.rb +3 -0
- data/lib/universal_id/contrib/global_id/global_id_model.rb +24 -0
- data/lib/universal_id/contrib/global_id/global_id_uid_extension.rb +36 -0
- data/lib/universal_id/contrib/global_id/message_pack_type.rb +15 -0
- data/lib/universal_id/contrib/global_id.rb +3 -0
- data/lib/universal_id/contrib/rails.rb +6 -0
- data/lib/universal_id/contrib/signed_global_id/message_pack_type.rb +8 -0
- data/lib/universal_id/contrib/signed_global_id.rb +3 -0
- data/lib/universal_id/encoder.rb +27 -0
- data/lib/universal_id/message_pack_factory.rb +51 -0
- data/lib/universal_id/message_pack_types/ruby/composites/open_struct.rb +8 -0
- data/lib/universal_id/message_pack_types/ruby/composites/set.rb +9 -0
- data/lib/universal_id/message_pack_types/ruby/composites/struct.rb +23 -0
- data/lib/universal_id/message_pack_types/ruby/scalars/complex.rb +8 -0
- data/lib/universal_id/message_pack_types/ruby/scalars/date.rb +8 -0
- data/lib/universal_id/message_pack_types/ruby/scalars/date_time.rb +8 -0
- data/lib/universal_id/message_pack_types/ruby/scalars/range.rb +20 -0
- data/lib/universal_id/message_pack_types/ruby/scalars/rational.rb +8 -0
- data/lib/universal_id/message_pack_types/ruby/scalars/regexp.rb +15 -0
- data/lib/universal_id/message_pack_types/uri/uid/type.rb +8 -0
- data/lib/universal_id/message_pack_types.rb +24 -0
- data/lib/universal_id/prepack_database_options.rb +63 -0
- data/lib/universal_id/prepack_options.rb +74 -0
- data/lib/universal_id/prepacker.rb +28 -0
- data/lib/universal_id/refinements/array_refinement.rb +17 -0
- data/lib/universal_id/refinements/hash_refinement.rb +19 -0
- data/lib/universal_id/refinements/kernel_refinement.rb +19 -0
- data/lib/universal_id/refinements/open_struct_refinement.rb +12 -0
- data/lib/universal_id/refinements/set_refinement.rb +12 -0
- data/lib/universal_id/refinements.rb +9 -0
- data/lib/universal_id/settings.rb +82 -0
- data/lib/universal_id/version.rb +1 -1
- data/lib/universal_id.rb +25 -10
- data/lib/uri/uid.rb +95 -0
- metadata +105 -28
- data/lib/universal_id/active_model_serializer.rb +0 -53
- data/lib/universal_id/config.rb +0 -18
- data/lib/universal_id/errors.rb +0 -11
- data/lib/universal_id/portable.rb +0 -24
- 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
|
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-
|
5
|
+
<img alt="Lines of Code" src="https://img.shields.io/badge/loc-721-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/
|
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,1018 @@
|
|
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">
|
34
|
-
<h3 align="center">Simple, standardized, secure marshaling for peace-of-mind object portability</h3>
|
36
|
+
<h2 align="center">URL-Safe Portability for any Ruby Object</h2>
|
35
37
|
</p>
|
36
38
|
|
37
|
-
|
38
|
-
|
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
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
- [
|
57
|
-
- [
|
58
|
-
|
59
|
-
- [
|
60
|
-
- [
|
61
|
-
- [
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
64
|
+
- [Supported Data Types](#supported-data-types)
|
65
|
+
- [Scalars](#scalars)
|
66
|
+
- [Composites](#composites)
|
67
|
+
- [Contributed Types](#contributed-types)
|
68
|
+
- [ActiveRecord](#activerecord)
|
69
|
+
- [Why Universal ID with ActiveRecord?](#why-universal-id-with-activerecord)
|
70
|
+
- [Custom Datatypes](#custom-datatypes)
|
71
|
+
- [Settings and Prepack Options](#settings-and-prepack-options)
|
72
|
+
- [Advanced ActiveRecord](#advanced-activerecord)
|
73
|
+
- [ActiveRecord::Relation Support](#activerecordrelation-support)
|
74
|
+
- [SignedGlobalID](#signedglobalid)
|
75
|
+
- [Performance and Benchmarks](#performance-and-benchmarks)
|
76
|
+
- [Sponsors](#sponsors)
|
66
77
|
- [License](#license)
|
67
78
|
|
68
79
|
<!-- Tocer[finish]: Auto-generated, don't remove. -->
|
69
80
|
|
70
|
-
|
81
|
+
> :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!
|
82
|
+
|
83
|
+
|
84
|
+
## Supported Data Types
|
85
|
+
|
86
|
+
### Scalars
|
71
87
|
|
72
|
-
|
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.
|
88
|
+
Universal ID supports most Ruby primitives.
|
76
89
|
|
77
|
-
|
78
|
-
|
90
|
+
- `NilClass`
|
91
|
+
- `Complex`
|
92
|
+
- `Date`
|
93
|
+
- `DateTime`
|
94
|
+
- `FalseClass`
|
95
|
+
- `Float`
|
96
|
+
- `Integer`
|
97
|
+
- `NilClass`
|
98
|
+
- `Range`
|
99
|
+
- `Rational`
|
100
|
+
- `Regexp`
|
101
|
+
- `String`
|
102
|
+
- `Symbol`
|
103
|
+
- `Time`
|
104
|
+
- `TrueClass`
|
79
105
|
|
80
|
-
|
106
|
+
You can use Universal ID for individual primitives if desired, but scalar support is really the foundation for more serious use cases.
|
107
|
+
_See below..._
|
81
108
|
|
82
109
|
```ruby
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
campaign == gid.find # true
|
110
|
+
uri = URI::UID.build(:demo).to_s
|
111
|
+
#=> "uid://universal-id/iwKA1gBkZW1vAw"
|
112
|
+
|
113
|
+
uid = URI::UID.parse(uri)
|
114
|
+
#=> #<URI::UID uid://universal-id/iwKA1gBkZW1vAw>
|
115
|
+
|
116
|
+
uid.decode
|
117
|
+
#=> :demo
|
92
118
|
```
|
93
119
|
|
120
|
+
### Composites
|
121
|
+
|
122
|
+
Composite support is where things start to get interesting. All of the composite datatypes listed below can be recursively transformed into a Universal ID.
|
123
|
+
|
124
|
+
<details>
|
125
|
+
<summary><b><code>[]</code> Array</b>... ▾</summary>
|
126
|
+
<p></p>
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
array = [1, 2, 3, [:a, :b, :c, [true]]]
|
130
|
+
|
131
|
+
uri = URI::UID.build(array).to_s
|
132
|
+
#=> "uid://universal-id/iweAlAECA5TUAGHUAGLUAGORwwM"
|
133
|
+
|
134
|
+
uid = URI::UID.parse(uri)
|
135
|
+
#=> #<URI::UID uid://universal-id/iweAlAECA5TUAGHUAGLUAGORwwM>
|
136
|
+
|
137
|
+
uid.decode
|
138
|
+
#=> [1, 2, 3, [:a, :b, :c, [true]]]
|
139
|
+
```
|
140
|
+
</details>
|
141
|
+
|
142
|
+
<details>
|
143
|
+
<summary><b><code>{}</code> Hash</b>... ▾</summary>
|
144
|
+
<p></p>
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
hash = {a: 1, b: 2, c: 3, array: [1, 2, 3, [:a, :b, :c, [true]]]}
|
148
|
+
|
149
|
+
uri = URI::UID.build(hash).to_s
|
150
|
+
#=> "uid://universal-id/CxKAhNQAYQHUAGIC1ABjA8cFAGFycmF5lAEC..."
|
151
|
+
|
152
|
+
uid = URI::UID.parse(uri)
|
153
|
+
#=> #<URI::UID uid://universal-id/CxKAhNQAYQHUAGIC1ABjA8cFAGFycmF5lAECA5TUAGHUAGLUAGORwwM>
|
154
|
+
|
155
|
+
uid.decode
|
156
|
+
#=> {:a=>1, :b=>2, :c=>3, :array=>[1, 2, 3, [:a, :b, :c, [true]]]}
|
157
|
+
```
|
158
|
+
</details>
|
159
|
+
|
160
|
+
<details>
|
161
|
+
<summary><b><code><></code> Open Struct</b>... ▾</summary>
|
162
|
+
<p></p>
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
ostruct = OpenStruct.new(
|
166
|
+
name: "Wireless Keyboard",
|
167
|
+
price: 49.99,
|
168
|
+
category: "Electronics",
|
169
|
+
in_stock: true
|
170
|
+
)
|
171
|
+
|
172
|
+
uri = URI::UID.build(ostruct).to_s
|
173
|
+
#=> "uid://universal-id/iyaAx0sMhNYAbmFtZbFXaXJlbGVzcyBLZXlib2FyZMcFAHByaWNly0BI_rhR64Uf1wBjYXRlZ29ye..."
|
174
|
+
|
175
|
+
uid = URI::UID.parse(uri)
|
176
|
+
#=> #<URI::UID scheme=uid, host=universal-id, payload=iyaAx0sMhNYAbmFtZbFXaXJlbGVzcyBLZXlib2FyZMcFAHByaWNly0BI_rhR64Uf1wBjYXRlZ29ye...>
|
177
|
+
|
178
|
+
uid.decode
|
179
|
+
#=> #<OpenStruct name="Wireless Keyboard", price=49.99, category="Electronics", in_stock=true>
|
180
|
+
```
|
181
|
+
</details>
|
182
|
+
|
183
|
+
<details>
|
184
|
+
<summary><b><code>()</code> Set</b>... ▾</summary>
|
185
|
+
<p></p>
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
set = Set.new([1, 2, 3, [:a, :b, :c, [true]]])
|
189
|
+
|
190
|
+
uri = URI::UID.build(set).to_s
|
191
|
+
#=> "uid://universal-id/iwiA2AuUAQIDlNQAYdQAYtQAY5HDAw"
|
192
|
+
|
193
|
+
uid = URI::UID.parse(uri)
|
194
|
+
#=> #<URI::UID uid://universal-id/iwiA2AuUAQIDlNQAYdQAYtQAY5HDAw>
|
195
|
+
|
196
|
+
URI::UID.parse(uri).decode
|
197
|
+
#=> #<Set: {1, 2, 3, [:a, :b, :c, [true]]}>
|
198
|
+
```
|
199
|
+
</details>
|
200
|
+
|
201
|
+
<details>
|
202
|
+
<summary><b><code><></code> Struct</b>... ▾</summary>
|
203
|
+
<p></p>
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
Book = Struct.new(:title, :author, :isbn, :published_year)
|
207
|
+
book = Book.new("The Great Gatsby", "F. Scott Fitzgerald", "9780743273565", 1925)
|
208
|
+
|
209
|
+
uri = URI::UID.build(book).to_s
|
210
|
+
#=> "uid://universal-id/G2YAoGTomv9N_4RV2oJRxRvZdC1wNJ0H3Ipu45kVcSrAxtg6Wjtogpi6GV1XXQAOAXoNR3BrCg9AQ..."
|
211
|
+
|
212
|
+
uid = URI::UID.parse(uri)
|
213
|
+
#=> #<URI::UID scheme=uid, host=universal-id, payload=G2YAoGTomv9N_4RV2oJRxRvZdC1wNJ0H3Ipu45kVcSrAxtg6Wjtogpi6GV1XXQAOAXoNR3BrCg9AQ...>
|
214
|
+
|
215
|
+
uid.decode
|
216
|
+
#=> #<struct Book title="The Great Gatsby", author="F. Scott Fitzgerald", isbn="9780743273565", published_year=1925>
|
217
|
+
```
|
218
|
+
</details>
|
219
|
+
|
220
|
+
### Contributed Types
|
221
|
+
|
222
|
+
Universal ID is designed to be highly extensible, allowing for third-party contributions to enhance its capabilities.
|
223
|
+
These contributions can introduce support for additional data types, further broadening the scope of Universal ID’s utility.
|
224
|
+
The following are some notable contrib extensions:
|
225
|
+
|
226
|
+
- **ActiveRecord::Base**: Integrates Universal ID with ActiveRecord base models, enabling intelligent serialization of database records
|
227
|
+
- **ActiveRecord::Relation**: Supports the serialization of ActiveRecord relations, making it possible to encode complex query structures
|
228
|
+
- **ActiveSupport::TimeWithZone**: Adds the ability to serialize ActiveSupport's TimeWithZone objects
|
229
|
+
- **GlobalID**: Extends support to include GlobalIDs
|
230
|
+
- **SignedGlobalID**: Extends support to include SignedGlobalIDs
|
231
|
+
|
232
|
+
#### Requiring Contributed Types
|
233
|
+
|
234
|
+
To utilize the contributed types, you must explicitly require them in your application.
|
235
|
+
This ensures the extensions are loaded and available for use.
|
236
|
+
Here is an example illustrating how to include contributed types:
|
237
|
+
|
94
238
|
```ruby
|
95
|
-
#
|
96
|
-
|
97
|
-
|
98
|
-
|
239
|
+
# load contrib types
|
240
|
+
require "universal_id/contrib/active_record"
|
241
|
+
require "universal_id/contrib/active_support"
|
242
|
+
require "universal_id/contrib/global_id"
|
243
|
+
require "universal_id/contrib/signed_global_id"
|
99
244
|
|
100
|
-
|
101
|
-
|
245
|
+
# or simply
|
246
|
+
require "universal_id/contrib/rails"
|
102
247
|
```
|
103
248
|
|
104
|
-
|
249
|
+
> :bulb: **Implicit Contribs**: Whenever the `Rails` constant is defined, the related contribs are auto-loaded.
|
105
250
|
|
106
|
-
|
251
|
+
### ActiveRecord
|
107
252
|
|
108
|
-
-
|
109
|
-
- Hash
|
110
|
-
- ActiveModel _(unsaved)_
|
111
|
-
- ActiveRecord::Relation
|
112
|
-
- etc.
|
253
|
+
> :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**.
|
113
254
|
|
114
|
-
|
255
|
+
#### Why Universal ID with ActiveRecord?
|
115
256
|
|
116
|
-
|
117
|
-
Consider a multi-step form or wizard where users incrementally build up a complex set of related ActiveRecord instances.
|
257
|
+
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.
|
118
258
|
|
119
|
-
-
|
120
|
-
-
|
121
|
-
-
|
122
|
-
-
|
123
|
-
-
|
124
|
-
-
|
259
|
+
- **Support for New Records**: Unlike GlobalID, Universal ID can serialize models that haven't been saved to the database yet
|
260
|
+
- **Capturing Unsaved Changes**: It can serialize ActiveRecord models with unsaved changes, ensuring that even transient states are captured
|
261
|
+
- **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
|
262
|
+
- **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
|
263
|
+
- **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
|
264
|
+
- **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.
|
125
265
|
|
126
|
-
|
266
|
+
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.
|
127
267
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
param = campaign.to_portable_hash_sgid_param #... make it portable (assign this to a hidden field, querystrig etc.)
|
268
|
+
<details>
|
269
|
+
<summary><b>How to Convert Records to UIDs</b>... ▾</summary>
|
270
|
+
<p></p>
|
132
271
|
|
133
|
-
|
272
|
+
```ruby
|
273
|
+
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
134
274
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
275
|
+
ActiveRecord::Schema.define do
|
276
|
+
create_table :campaigns do |t|
|
277
|
+
t.column :name, :string
|
278
|
+
t.timestamps
|
279
|
+
end
|
280
|
+
end
|
139
281
|
|
140
|
-
|
282
|
+
class Campaign < ApplicationRecord
|
283
|
+
end
|
141
284
|
|
142
|
-
#
|
143
|
-
# ...
|
285
|
+
# ---
|
144
286
|
|
145
|
-
|
146
|
-
campaign = Campaign.new_from_portable_hash(param)
|
147
|
-
campaign.save!
|
148
|
-
```
|
287
|
+
campaign = Campaign.create(name: "Marketing Campaign")
|
149
288
|
|
150
|
-
|
289
|
+
uri = URI::UID.build(campaign).to_s
|
290
|
+
#=> "uid://universal-id/CwiAxw4EqENhbXBhaWdugaJpZAMD"
|
151
291
|
|
152
|
-
|
292
|
+
uid = URI::UID.parse(uri)
|
293
|
+
#=> #<URI::UID uid://universal-id/CwiAxw4EqENhbXBhaWdugaJpZAMD>
|
153
294
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
295
|
+
URI::UID.parse(uri).decode
|
296
|
+
##<Campaign:0x000000011cc67da8 id: 1, name: "Marketing Campaign", ...>
|
297
|
+
```
|
298
|
+
</details>
|
158
299
|
|
159
|
-
###
|
300
|
+
### Custom Datatypes
|
160
301
|
|
161
|
-
|
302
|
+
Universal ID is **extensible** so you can register your own datatypes with specialized serialization rules.
|
303
|
+
It couldn't be simpler. Just convert the required data to a Ruby scalar or composite value.
|
162
304
|
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
305
|
+
<details>
|
306
|
+
<summary><b>How to Register your own Datatype</b>... ▾</summary>
|
307
|
+
<p></p>
|
170
308
|
|
171
|
-
|
309
|
+
```ruby
|
310
|
+
class UserSettings
|
311
|
+
attr_accessor :user_id, :preferences
|
172
312
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
313
|
+
def initialize(user_id, preferences = {})
|
314
|
+
@user_id = user_id
|
315
|
+
@preferences = preferences
|
316
|
+
end
|
317
|
+
end
|
178
318
|
|
179
|
-
|
319
|
+
UniversalID::MessagePackFactory.register(
|
320
|
+
type: UserSettings,
|
321
|
+
packer: ->(user_preferences, packer) do
|
322
|
+
packer.write user_preferences.user_id
|
323
|
+
packer.write user_preferences.preferences
|
324
|
+
end,
|
325
|
+
unpacker: ->(unpacker) do
|
326
|
+
user_id = unpacker.read
|
327
|
+
preferences = unpacker.read
|
328
|
+
UserSettings.new user_id, preferences
|
329
|
+
end
|
330
|
+
)
|
180
331
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
332
|
+
settings = UserSettings.new(1,
|
333
|
+
theme: "dark",
|
334
|
+
notifications: "email",
|
335
|
+
language: "en",
|
336
|
+
layout: "grid",
|
337
|
+
privacy: "private"
|
338
|
+
)
|
186
339
|
|
340
|
+
uri = URI::UID.build(settings).to_s
|
341
|
+
#=> "uid://universal-id/G1QAQAT-bfcGW1QOgadJwJF06yL8gDnGgfs1Xdti20TDDvG5STPqzbYcQ6TBqVKhdZ39CdQZUwEGe..."
|
187
342
|
|
188
|
-
|
189
|
-
|
343
|
+
uid = URI::UID.parse(uri)
|
344
|
+
#=> #<URI::UID uid://universal-id/G1QAQAT-bfcGW1QOgadJwJF06yL8gDnGgfs1Xdti20TDDvG5STPqzbYcQ6TBqVKhdZ39CdQZUwEGe..."
|
190
345
|
|
346
|
+
uid.decode
|
347
|
+
=> #<UserSettings:0x0000000139157dd8 @preferences={:theme=>"dark", :notifications=>"email", :language=>"en", :layout=>"grid", :privacy=>"private"}, @user_id=1>
|
348
|
+
```
|
349
|
+
</details>
|
191
350
|
|
192
|
-
|
193
|
-
{"name"=>"Example", "list"=>[1, 2, 3], "object"=>{"nested"=>true}}
|
194
|
-
```
|
351
|
+
## Settings and Prepack Options
|
195
352
|
|
196
|
-
|
353
|
+
Universal ID supports a small but powerful set of configuration options for transforming objects before being
|
354
|
+
handed off to MessagePack for serialization.
|
197
355
|
|
198
|
-
|
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...
|
356
|
+
Prepacking gives you explicit control over what data to include in the Universal ID.
|
202
357
|
|
203
|
-
copy = Email.new_from_portable_hash(gid)
|
204
|
-
signed_copy = Email.new_from_portable_hash(sgid)
|
205
358
|
|
206
|
-
|
359
|
+
<details>
|
360
|
+
<summary><b>View All Settings and Prepack Options</b>... ▾</summary>
|
361
|
+
<p></p>
|
207
362
|
|
208
|
-
|
209
|
-
|
363
|
+
```yml
|
364
|
+
prepack:
|
365
|
+
# ..........................................................................................................
|
366
|
+
# A list of attributes to exclude (for objects like Hash, OpenStruct, Struct, etc.)
|
367
|
+
# Takes prescedence over the`include` list
|
368
|
+
exclude: []
|
210
369
|
|
211
|
-
|
212
|
-
|
370
|
+
# ..........................................................................................................
|
371
|
+
# A list of attributes to include (for objects like Hash, OpenStruct, Struct, etc.)
|
372
|
+
include: []
|
213
373
|
|
214
|
-
#
|
215
|
-
|
216
|
-
|
217
|
-
```
|
374
|
+
# ..........................................................................................................
|
375
|
+
# Whether or not to omit blank values when packing (nil, {}, [], "", etc.)
|
376
|
+
include_blank: true
|
218
377
|
|
219
|
-
|
378
|
+
# ==========================================================================================================
|
379
|
+
# Database records
|
380
|
+
database:
|
381
|
+
# ......................................................................................................
|
382
|
+
# Whether or not to include primary/foreign keys
|
383
|
+
# Setting this to `false` can be used to make a copy of an existing record
|
384
|
+
include_keys: true
|
220
385
|
|
221
|
-
|
222
|
-
#
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
386
|
+
# ......................................................................................................
|
387
|
+
# Whether or not to include date/time timestamps (created_at, updated_at, etc.)
|
388
|
+
# Setting this to `false` can be used to make a copy of an existing record
|
389
|
+
include_timestamps: true
|
390
|
+
|
391
|
+
# ......................................................................................................
|
392
|
+
# Whether or not to include unsaved changes
|
393
|
+
# Assign to `true` when packing new records
|
394
|
+
include_unsaved_changes: false
|
395
|
+
|
396
|
+
# ......................................................................................................
|
397
|
+
# Whether or not to include loaded in-memory descendants (i.e. child associations)
|
398
|
+
include_descendants: false
|
399
|
+
|
400
|
+
# ......................................................................................................
|
401
|
+
# The max depth (number) of loaded in-memory descendants to include when `include_descendants == true`
|
402
|
+
# For example, a value of (3) would include the following:
|
403
|
+
# Parent > Child > Grandchild
|
404
|
+
descendant_depth: 0
|
405
|
+
```
|
406
|
+
</details>
|
407
|
+
|
408
|
+
Prepack options can be applied when creating a Universal ID and can be passed in structured or flat format.
|
409
|
+
|
410
|
+
<details>
|
411
|
+
<summary><b>How to Apply Prepack Options when Creating UIDs</b>... ▾</summary>
|
412
|
+
<p></p>
|
413
|
+
|
414
|
+
```ruby
|
415
|
+
person = {
|
416
|
+
full_name: "Jane Doe",
|
417
|
+
email: "janedoe@example.com",
|
418
|
+
birthdate: "1980-05-15",
|
419
|
+
phone_number: "555-6789",
|
420
|
+
ssn: "123-45-6789",
|
421
|
+
children: [
|
422
|
+
{
|
423
|
+
full_name: "Alice Doe",
|
424
|
+
email: "alicedoe@example.com",
|
425
|
+
birthdate: nil,
|
426
|
+
phone_number: "555-1234",
|
427
|
+
ssn: "987-65-4321"
|
428
|
+
},
|
429
|
+
{
|
430
|
+
full_name: "Bob Doe",
|
431
|
+
email: "bobdoe@example.com",
|
432
|
+
birthdate: "2008-11-21",
|
433
|
+
phone_number: nil,
|
434
|
+
ssn: "456-12-1234"
|
435
|
+
}
|
436
|
+
]
|
437
|
+
}
|
438
|
+
|
439
|
+
uid = URI::UID.build(person, include_blank: false, exclude: [:phone_number, :ssn])
|
440
|
+
uid.decode
|
441
|
+
|
442
|
+
# Note that the decoded payload is smaller due to the prepack options
|
443
|
+
# Also note that the options were applied recursively
|
444
|
+
|
445
|
+
{
|
446
|
+
full_name: "Jane Doe",
|
447
|
+
email: "janedoe@example.com",
|
448
|
+
birthdate: "1980-05-15",
|
449
|
+
children: [
|
450
|
+
{
|
451
|
+
full_name: "Alice Doe",
|
452
|
+
email: "alicedoe@example.com"
|
453
|
+
},
|
454
|
+
{
|
455
|
+
full_name: "Bob Doe",
|
456
|
+
email: "bobdoe@example.com",
|
457
|
+
birthdate: "2008-11-21"
|
458
|
+
}
|
459
|
+
]
|
460
|
+
}
|
461
|
+
```
|
462
|
+
</details>
|
463
|
+
|
464
|
+
It's also possible to register frequently used options as reusable settings to further simplify creating UIDs.
|
465
|
+
|
466
|
+
<details>
|
467
|
+
<summary><b>How to Register Prepack Options as Preconfigured Settings</b>... ▾</summary>
|
468
|
+
<p></p>
|
469
|
+
|
470
|
+
```yaml
|
471
|
+
# app/config/unsaved.yml
|
472
|
+
prepack:
|
473
|
+
include_blank: false
|
474
|
+
|
475
|
+
database:
|
476
|
+
include_unsaved_changes: true
|
477
|
+
include_timestamps: false
|
478
|
+
```
|
479
|
+
|
480
|
+
```ruby
|
481
|
+
UniversalID::Settings.register :unsaved, YAML.safe_load("app/config/unsaved.yml")
|
482
|
+
URI::UID.build @record, UniversalID::Settings[:small_record]
|
483
|
+
```
|
484
|
+
</details>
|
485
|
+
|
486
|
+
## Advanced ActiveRecord
|
487
|
+
|
488
|
+
Universal ID includes some advanced capabilities when used with ActiveRecord.
|
489
|
+
|
490
|
+
- [x] **Include loaded associations**
|
491
|
+
Universal ID supports including `loaded` associations when a model is transformed into a UID.
|
492
|
+
<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>
|
493
|
+
|
494
|
+
- [x] **Include unsaved changes**
|
495
|
+
Universal ID supports capturing unsaved change, for both new and persisted records, when a model is transformed into a UID.
|
496
|
+
<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>
|
497
|
+
|
498
|
+
- [x] **Exclude keys** to make copies of existing records
|
499
|
+
Universal ID supports making copies of individual records or entire collections by opt'ing to exclude keys when transorming to UID.
|
500
|
+
<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>
|
501
|
+
|
502
|
+
First, let's establish the schema structure and data we'll be working with.
|
503
|
+
We'll limit ourselves to 3 tables here, but Universal ID can support much more complex data models.
|
504
|
+
|
505
|
+
- Campaign
|
506
|
+
- Email
|
507
|
+
- Attachment
|
508
|
+
|
509
|
+
We'll use 1 campaign with 3 emails that have 2 attachments each.
|
510
|
+
|
511
|
+
<details>
|
512
|
+
<summary><b>Setup the Schema</b>... ▾</summary>
|
513
|
+
<p></p>
|
514
|
+
|
515
|
+
```ruby
|
516
|
+
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
517
|
+
|
518
|
+
ActiveRecord::Schema.define do
|
519
|
+
create_table :campaigns do |t|
|
520
|
+
t.column :name, :string
|
521
|
+
t.column :description, :text
|
522
|
+
t.column :trigger, :string
|
523
|
+
t.timestamps
|
524
|
+
end
|
525
|
+
|
526
|
+
create_table :emails do |t|
|
527
|
+
t.column :campaign_id, :integer
|
528
|
+
t.column :subject, :string
|
529
|
+
t.column :body, :text
|
530
|
+
t.column :wait, :integer
|
531
|
+
t.timestamps
|
532
|
+
end
|
533
|
+
|
534
|
+
create_table :attachments do |t|
|
535
|
+
t.column :email_id, :integer
|
536
|
+
t.column :file_name, :string
|
537
|
+
t.column :content_type, :string
|
538
|
+
t.column :file_size, :integer
|
539
|
+
t.column :file_data, :binary
|
540
|
+
t.timestamps
|
541
|
+
end
|
542
|
+
end
|
543
|
+
```
|
544
|
+
</details>
|
545
|
+
|
546
|
+
<details>
|
547
|
+
<summary><b>Setup the Models</b>... ▾</summary>
|
548
|
+
<p></p>
|
549
|
+
|
550
|
+
```ruby
|
551
|
+
class Campaign < ApplicationRecord
|
552
|
+
has_many :emails, dependent: :destroy
|
553
|
+
end
|
554
|
+
|
555
|
+
class Email < ApplicationRecord
|
556
|
+
belongs_to :campaign
|
557
|
+
has_many :attachments, dependent: :destroy
|
558
|
+
end
|
559
|
+
|
560
|
+
class Attachment < ApplicationRecord
|
561
|
+
belongs_to :email
|
562
|
+
end
|
563
|
+
```
|
564
|
+
</details>
|
565
|
+
|
566
|
+
|
567
|
+
<details>
|
568
|
+
<summary><b>Setup the Model Instances</b>... ▾</summary>
|
569
|
+
<p></p>
|
570
|
+
|
571
|
+
```ruby
|
572
|
+
campaign = Campaign.new(
|
573
|
+
name: "Summer Sale Campaign",
|
574
|
+
description: "A campaign for the summer sale, targeting our loyal customers.",
|
575
|
+
trigger: "SummerStart"
|
576
|
+
)
|
577
|
+
|
578
|
+
# NOTE: Assigning campaign.emails via `=` to ensure ActiveRecord flags the association as `loaded`
|
579
|
+
campaign.emails = 3.times.map do |i|
|
580
|
+
email = campaign.emails.build(
|
581
|
+
subject: "Summer Sale Special Offer #{i + 1}",
|
582
|
+
body: "Dear Customer, check out our exclusive summer sale offers! #{i + 1}",
|
583
|
+
wait: rand(1..14)
|
584
|
+
)
|
585
|
+
|
586
|
+
# NOTE: Assigning email.attachments via `=` to ensure ActiveRecord flags the association as `loaded`
|
587
|
+
email.tap do |e|
|
588
|
+
e.attachments = 2.times.map do |j|
|
589
|
+
data = SecureRandom.random_bytes(rand(500..1500))
|
590
|
+
e.attachments.build(
|
591
|
+
file_name: "summer_sale_#{i + 1}_attachment_#{j + 1}.pdf",
|
592
|
+
content_type: "application/pdf",
|
593
|
+
file_size: data.size,
|
594
|
+
file_data: data
|
595
|
+
)
|
596
|
+
end
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
# demonstrate that we have new unsaved records
|
601
|
+
|
602
|
+
#campaign
|
603
|
+
campaign.new_record? # true
|
604
|
+
campaign.changed? # true
|
605
|
+
|
606
|
+
# emails
|
607
|
+
campaign.emails.each do |email|
|
608
|
+
email.new_record? # true
|
609
|
+
email.changed? # true
|
610
|
+
|
611
|
+
email.attachments.each do |attachment|
|
612
|
+
attachment.new_record? # true
|
613
|
+
attachment.changed? # true
|
614
|
+
end
|
615
|
+
end
|
616
|
+
```
|
617
|
+
</details>
|
618
|
+
|
619
|
+
Now let's look at how to leverage Universal ID with ActiveRecord.
|
620
|
+
|
621
|
+
<details>
|
622
|
+
<summary><b>How to Include Unsaved Changes for New Records</b>... ▾</summary>
|
623
|
+
<p></p>
|
624
|
+
|
625
|
+
```ruby
|
626
|
+
# prepack options
|
627
|
+
options = {
|
628
|
+
include_unsaved_changes: true,
|
629
|
+
include_descendants: true,
|
630
|
+
descendant_depth: 2
|
631
|
+
}
|
632
|
+
|
633
|
+
# NOTE: The campaign model instance was setup earlier in the "Model Instances" section above
|
634
|
+
campaign.new_record? # true
|
635
|
+
campaign.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
|
+
campaign.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
|
+
encoded = URI::UID.build(campaign, options).to_s
|
664
|
+
restored = URI::UID.parse(encoded).decode
|
665
|
+
|
666
|
+
restored.new_record? # true
|
667
|
+
restored.changes
|
668
|
+
# {
|
669
|
+
# "name"=>[nil, "Summer Sale Campaign"],
|
670
|
+
# "description"=>[nil, "A campaign for the summer sale, targeting our loyal customers."],
|
671
|
+
# "trigger"=>[nil, "SummerStart"]
|
672
|
+
# }
|
673
|
+
|
674
|
+
restored.emails.each do |email|
|
675
|
+
email.new_record? # true
|
676
|
+
email.changes
|
677
|
+
# {
|
678
|
+
# "subject"=>[nil, "Summer Sale Special Offer ..."],
|
679
|
+
# "body"=>[nil, "Dear Customer, check out our exclusive summer sale offers! ..."],
|
680
|
+
# "wait"=>[nil, ...]
|
681
|
+
# }
|
682
|
+
|
683
|
+
email.attachments.each do |attachment|
|
684
|
+
attachment.new_record? # true
|
685
|
+
attachment.changes
|
686
|
+
# {
|
687
|
+
# "file_name"=>[nil, "summer_sale_..._attachment_....pdf"],
|
688
|
+
# "content_type"=>[nil, "application/pdf"],
|
689
|
+
# "file_size"=>[nil, ...],
|
690
|
+
# "file_data"=>[nil, "..."]
|
691
|
+
# }
|
692
|
+
end
|
693
|
+
end
|
694
|
+
```
|
695
|
+
</details>
|
696
|
+
|
697
|
+
<details>
|
698
|
+
<summary><b>How to Include Unsaved Changes for Persisted Records</b>... ▾</summary>
|
699
|
+
<p></p>
|
700
|
+
|
701
|
+
```ruby
|
702
|
+
# NOTE: The campaign model instance was setup earlier in the "Model Instances" section above
|
703
|
+
# persist the model and its associations
|
704
|
+
campaign.save!
|
705
|
+
|
706
|
+
# make some unsaved changes to the records
|
707
|
+
campaign.name = "Changed Name #{SecureRandom.hex}"
|
708
|
+
campaign.emails.each do |email|
|
709
|
+
email.subject = "Changed Subject #{SecureRandom.hex}"
|
710
|
+
email.attachments.each do |attachment|
|
711
|
+
attachment.file_name = "changed_file_name#{SecureRandom.hex}.pdf"
|
712
|
+
end
|
713
|
+
end
|
714
|
+
|
715
|
+
campaign.persisted? # true
|
716
|
+
campaign.changes
|
717
|
+
# {"name"=>["Summer Sale Campaign", "Changed Name ..."]}
|
718
|
+
|
719
|
+
campaign.emails.each do |email|
|
720
|
+
email.persisted? # true
|
721
|
+
email.changes
|
722
|
+
# {"subject"=>["Summer Sale Special Offer 1", "Changed Subject ..."]}
|
723
|
+
|
724
|
+
email.attachments.each do |attachment|
|
725
|
+
attachment.persisted? # true
|
726
|
+
attachment.changes
|
727
|
+
# {"file_name"=>["summer_sale_..._attachment_....pdf", "changed_file_name....pdf"]}
|
728
|
+
end
|
729
|
+
end
|
730
|
+
|
731
|
+
# prepack options
|
732
|
+
options = {
|
733
|
+
include_unsaved_changes: true,
|
734
|
+
include_descendants: true,
|
735
|
+
descendant_depth: 2
|
736
|
+
}
|
737
|
+
|
738
|
+
encoded = URI::UID.build(campaign, options).to_s
|
739
|
+
restored = URI::UID.parse(encoded).decode
|
740
|
+
|
741
|
+
restored.persisted? # true
|
742
|
+
restored.changes
|
743
|
+
# {"name"=>["Summer Sale Campaign", "Changed Name ..."]}
|
744
|
+
|
745
|
+
restored.emails.each do |email|
|
746
|
+
email.persisted? # true
|
747
|
+
email.changes
|
748
|
+
# {"subject"=>["Summer Sale Special Offer 1", "Changed Subject ..."]}
|
749
|
+
|
750
|
+
email.attachments.each do |attachment|
|
751
|
+
attachment.persisted? # true
|
752
|
+
attachment.changes
|
753
|
+
# {"file_name"=>["summer_sale_..._attachment_....pdf", "changed_file_name....pdf"]}
|
754
|
+
end
|
755
|
+
end
|
756
|
+
```
|
757
|
+
</details>
|
758
|
+
|
759
|
+
<details>
|
760
|
+
<summary><b>How to Copy Persisted Records</b>... ▾</summary>
|
761
|
+
<p></p>
|
762
|
+
|
763
|
+
```ruby
|
764
|
+
# NOTE: The campaign model instance was setup earlier in the "Model Instances" section above
|
765
|
+
# persist the model and its associations
|
766
|
+
campaign.save!
|
767
|
+
|
768
|
+
options = {
|
769
|
+
include_keys: false,
|
770
|
+
include_timestamps: false,
|
771
|
+
include_unsaved_changes: true,
|
772
|
+
include_descendants: true,
|
773
|
+
descendant_depth: 2
|
774
|
+
}
|
775
|
+
|
776
|
+
encoded = URI::UID.build(campaign, options).to_s
|
777
|
+
copy = URI::UID.parse(encoded).decode
|
778
|
+
|
779
|
+
campaign.persisted? # false
|
780
|
+
copy.new_record? # true
|
781
|
+
copy.save!
|
782
|
+
|
783
|
+
copy == campaign # false
|
784
|
+
|
785
|
+
campaign.emails.each do |email|
|
786
|
+
copy_email_ids = copy.emails.map(&:id)
|
787
|
+
campaign_email_ids = campaign.emails.map(&:id)
|
788
|
+
(copy_email_ids && campaign_email_ids).any? # false
|
789
|
+
|
790
|
+
copy_attachment_ids = copy.emails.map(&:attachments).flatten.map(&:id)
|
791
|
+
campaign_attachment_ids = campaign.emails.map(&:attachments).flatten.map(&:id)
|
792
|
+
(copy_attachment_ids & campaign_attachment_ids).any? # false
|
793
|
+
end
|
794
|
+
```
|
795
|
+
</details>
|
796
|
+
|
797
|
+
## ActiveRecord::Relation Support
|
798
|
+
|
799
|
+
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.
|
800
|
+
|
801
|
+
> :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.
|
802
|
+
|
803
|
+
<details>
|
804
|
+
<summary><b>How to work with ActiveRecord::Relations</b>... ▾</summary>
|
805
|
+
<p></p>
|
806
|
+
|
807
|
+
```ruby
|
808
|
+
# Assuming we have multiple campaigns already stored in the database
|
809
|
+
relation = Campaign.joins(:emails).where("emails.subject LIKE ?", "Flash Sale%")
|
810
|
+
|
811
|
+
# force load the relation
|
812
|
+
relation.load
|
813
|
+
relation.loaded? # true
|
814
|
+
|
815
|
+
encoded = URI::UID.build(relation).to_s
|
816
|
+
decoded = URI::UID.parse(encoded).decode
|
817
|
+
|
818
|
+
decoded.is_a? ActiveRecord::Relation # true
|
819
|
+
decoded.loaded? # false
|
820
|
+
decoded == relation # true
|
821
|
+
decoded.size == relation.size # true
|
822
|
+
decoded.to_a == relation.to_a # true
|
823
|
+
```
|
824
|
+
</details>
|
825
|
+
|
826
|
+
## SignedGlobalID
|
827
|
+
|
828
|
+
Features like `signing` _(to prevent tampering)_, `purpose`, and `expiration` are provided by SignedGlobalIDs.
|
829
|
+
These features _(and more)_ will eventually be added to UniversalID, but until then...
|
830
|
+
simply convert your UniversalID to a SignedGlobalID to add these features to any Universal ID.
|
831
|
+
|
832
|
+
<details>
|
833
|
+
<summary><b>How to Convert a UID to/from a SignedGlobalID</b>... ▾</summary>
|
834
|
+
<p></p>
|
835
|
+
|
836
|
+
```ruby
|
837
|
+
product = {
|
838
|
+
name: "Wireless Bluetooth Headphones",
|
839
|
+
price: 79.99,
|
840
|
+
category: "Electronics"
|
841
|
+
}
|
842
|
+
|
843
|
+
uid = URI::UID.build(product)
|
844
|
+
#=> #<URI::UID scheme="uid", host="universal-id", path="/G0sAgBypU587HsjkLpEnGHiaWfPQEyiiuH6j...">
|
845
|
+
|
846
|
+
sgid = uid.to_sgid_param(for: "cart-123", expires_in: 1.hour)
|
847
|
+
#=> "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJZ0d4WjJsa09pOHZkVzVwZG1WeWMyRnNMV2xrTDFWU1NUbzZWVWxFT2pwSGJHOWlZV3hKUkZKbFkyOXlaQzlITUhO..."
|
848
|
+
|
849
|
+
URI::UID.from_sgid(sgid, for: "cart-123").decode
|
850
|
+
#=> {
|
851
|
+
# name: "Wireless Bluetooth Headphones",
|
852
|
+
# price: 79.99,
|
853
|
+
# category: "Electronics"
|
854
|
+
# }
|
855
|
+
|
856
|
+
|
857
|
+
# mismatched purpose returns nil... as expected
|
858
|
+
URI::UID.from_sgid(sgid, for: "mismatch")
|
859
|
+
#=> nil
|
860
|
+
```
|
861
|
+
</details>
|
862
|
+
|
863
|
+
## Performance and Benchmarks
|
864
|
+
|
865
|
+
<details>
|
866
|
+
<summary><b>View Benchmarks</b>... ▾</summary>
|
867
|
+
<p></p>
|
868
|
+
|
869
|
+
Benchmarks can be performed by cloning the project and running `bin/bench`.
|
870
|
+
The run below was performed on the following hardware.
|
871
|
+
|
872
|
+
```
|
873
|
+
Model Name: MacBook Air
|
874
|
+
Model Identifier: MacBookAir10,1
|
875
|
+
Chip: Apple M1
|
876
|
+
Total Number of Cores: 8 (4 performance and 4 efficiency)
|
877
|
+
Memory: 16 GB
|
878
|
+
```
|
879
|
+
|
880
|
+
```
|
881
|
+
Benchmarking with the following ActiveRecord/Hash data...
|
882
|
+
==================================================================================================
|
883
|
+
{
|
884
|
+
"id" => 1,
|
885
|
+
"name" => "Production",
|
886
|
+
"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.",
|
887
|
+
"trigger" => "Political Organization enhance web-enabled architectures",
|
888
|
+
"created_at" => "2023-11-11T01:28:46.657Z",
|
889
|
+
"updated_at" => "2023-11-11T01:28:46.657Z",
|
890
|
+
"emails" => [
|
891
|
+
[0] {
|
892
|
+
"id" => 1,
|
893
|
+
"campaign_id" => 1,
|
894
|
+
"subject" => "drive synergistic web-readiness",
|
895
|
+
"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?)",
|
896
|
+
"wait" => nil,
|
897
|
+
"created_at" => "2023-11-11T01:28:46.661Z",
|
898
|
+
"updated_at" => "2023-11-11T01:28:46.675Z",
|
899
|
+
"attachments" => [
|
900
|
+
[0] {
|
901
|
+
"id" => 1,
|
902
|
+
"email_id" => 1,
|
903
|
+
"file_name" => "Schneider and Sons",
|
904
|
+
"content_type" => "Enterprise-wide 4th generation complexity",
|
905
|
+
"file_size" => nil,
|
906
|
+
"file_data" => nil,
|
907
|
+
"created_at" => "2023-11-11T01:28:46.664Z",
|
908
|
+
"updated_at" => "2023-11-11T01:28:46.670Z"
|
909
|
+
},
|
910
|
+
[1] {
|
911
|
+
"id" => 2,
|
912
|
+
"email_id" => 1,
|
913
|
+
"file_name" => "Devolved solution-oriented circuit",
|
914
|
+
"content_type" => "revolutionize magnetic bandwidth Intelligent Paper Gloves",
|
915
|
+
"file_size" => nil,
|
916
|
+
"file_data" => nil,
|
917
|
+
"created_at" => "2023-11-11T01:28:46.664Z",
|
918
|
+
"updated_at" => "2023-11-11T01:28:46.670Z"
|
919
|
+
}
|
920
|
+
]
|
921
|
+
},
|
922
|
+
[1] {
|
923
|
+
"id" => 2,
|
924
|
+
"campaign_id" => 1,
|
925
|
+
"subject" => "Marketing",
|
926
|
+
"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.",
|
927
|
+
"wait" => nil,
|
928
|
+
"created_at" => "2023-11-11T01:28:46.671Z",
|
929
|
+
"updated_at" => "2023-11-11T01:28:46.675Z",
|
930
|
+
"attachments" => [
|
931
|
+
[0] {
|
932
|
+
"id" => 3,
|
933
|
+
"email_id" => 2,
|
934
|
+
"file_name" => "Weber-Schulist benchmark open-source applications",
|
935
|
+
"content_type" => "Enormous Linen Shoes synthesize customized e-services",
|
936
|
+
"file_size" => nil,
|
937
|
+
"file_data" => nil,
|
938
|
+
"created_at" => "2023-11-11T01:28:46.672Z",
|
939
|
+
"updated_at" => "2023-11-11T01:28:46.672Z"
|
940
|
+
},
|
941
|
+
[1] {
|
942
|
+
"id" => 4,
|
943
|
+
"email_id" => 2,
|
944
|
+
"file_name" => "thought leadership",
|
945
|
+
"content_type" => "Business Development Enhanced logistical collaboration",
|
946
|
+
"file_size" => nil,
|
947
|
+
"file_data" => nil,
|
948
|
+
"created_at" => "2023-11-11T01:28:46.672Z",
|
949
|
+
"updated_at" => "2023-11-11T01:28:46.672Z"
|
950
|
+
}
|
951
|
+
]
|
952
|
+
},
|
953
|
+
[2] {
|
954
|
+
"id" => 3,
|
955
|
+
"campaign_id" => 1,
|
956
|
+
"subject" => "Mediocre Aluminum Car",
|
957
|
+
"body" => "Don’t even trip dawg. Stay away from my special lady friend, man.",
|
958
|
+
"wait" => nil,
|
959
|
+
"created_at" => "2023-11-11T01:28:46.672Z",
|
960
|
+
"updated_at" => "2023-11-11T01:28:46.675Z",
|
961
|
+
"attachments" => [
|
962
|
+
[0] {
|
963
|
+
"id" => 5,
|
964
|
+
"email_id" => 3,
|
965
|
+
"file_name" => "Import and Export",
|
966
|
+
"content_type" => "Heavy Duty Paper Bench Project Management",
|
967
|
+
"file_size" => nil,
|
968
|
+
"file_data" => nil,
|
969
|
+
"created_at" => "2023-11-11T01:28:46.673Z",
|
970
|
+
"updated_at" => "2023-11-11T01:28:46.674Z"
|
971
|
+
},
|
972
|
+
[1] {
|
973
|
+
"id" => 6,
|
974
|
+
"email_id" => 3,
|
975
|
+
"file_name" => "synthesize ubiquitous architectures Corporate Communications",
|
976
|
+
"content_type" => "Durable Rubber Watch",
|
977
|
+
"file_size" => nil,
|
978
|
+
"file_data" => nil,
|
979
|
+
"created_at" => "2023-11-11T01:28:46.674Z",
|
980
|
+
"updated_at" => "2023-11-11T01:28:46.674Z"
|
981
|
+
}
|
982
|
+
]
|
983
|
+
}
|
984
|
+
]
|
985
|
+
}
|
986
|
+
==================================================================================================
|
987
|
+
Benchmarking 5000 iterations
|
988
|
+
==================================================================================================
|
989
|
+
user system total real
|
990
|
+
URI::UID.build Hash 14.770667 0.102535 14.873202 ( 14.898856)
|
991
|
+
Average 0.002954 0.000021 0.002975 ( 0.002980)
|
992
|
+
..................................................................................................
|
993
|
+
user system total real
|
994
|
+
URI::UID.build Hash, include_blank: false 13.821420 0.066910 13.888330 ( 13.892066)
|
995
|
+
Average 0.002764 0.000013 0.002778 ( 0.002778)
|
996
|
+
..................................................................................................
|
997
|
+
user system total real
|
998
|
+
URI::UID.parse HASH/UID 0.075566 0.000411 0.075977 ( 0.076035)
|
999
|
+
Average 0.000015 0.000000 0.000015 ( 0.000015)
|
1000
|
+
..................................................................................................
|
1001
|
+
user system total real
|
1002
|
+
URI::UID.decode HASH/UID 0.111007 0.003572 0.114579 ( 0.114587)
|
1003
|
+
Average 0.000022 0.000001 0.000023 ( 0.000023)
|
1004
|
+
..................................................................................................
|
1005
|
+
user system total real
|
1006
|
+
URI::UID.build ActiveRecord 0.984594 0.010059 0.994653 ( 0.994662)
|
1007
|
+
Average 0.000197 0.000002 0.000199 ( 0.000199)
|
1008
|
+
..................................................................................................
|
1009
|
+
user system total real
|
1010
|
+
URI::UID.build ActiveRecord, exclude_blank 0.953653 0.006692 0.960345 ( 0.960765)
|
1011
|
+
Average 0.000191 0.000001 0.000192 ( 0.000192)
|
1012
|
+
..................................................................................................
|
1013
|
+
user system total real
|
1014
|
+
URI::UID.build ActiveRecord, include_descendants 44.958468 0.170125 45.128593 ( 45.176116)
|
1015
|
+
Average 0.008992 0.000034 0.009026 ( 0.009035)
|
1016
|
+
..................................................................................................
|
1017
|
+
user system total real
|
1018
|
+
URI::UID.parse ActiveRecord/UID 0.119030 0.000319 0.119349 ( 0.119525)
|
1019
|
+
Average 0.000024 0.000000 0.000024 ( 0.000024)
|
1020
|
+
..................................................................................................
|
1021
|
+
user system total real
|
1022
|
+
URI::UID.decode HASH/UID 5.198092 0.024652 5.222744 ( 5.282794)
|
1023
|
+
Average 0.001040 0.000005 0.001045 ( 0.001057)
|
1024
|
+
..................................................................................................
|
1025
|
+
user system total real
|
1026
|
+
UID > GID > UID.decode include_descendants 55.612061 0.398193 56.010254 ( 57.372350)
|
1027
|
+
Average 0.011122 0.000080 0.011202 ( 0.011474)
|
1028
|
+
..................................................................................................
|
1029
|
+
user system total real
|
1030
|
+
UID > SGID > UID.decode include_descendants 55.406590 0.260552 55.667142 ( 56.432082)
|
1031
|
+
Average 0.011081 0.000052 0.011133 ( 0.011286)
|
1032
|
+
..................................................................................................
|
1033
|
+
```
|
1034
|
+
</details>
|
1035
|
+
|
1036
|
+
## Sponsors
|
1037
|
+
|
1038
|
+
<p align="center">
|
1039
|
+
<em>Proudly sponsored by</em>
|
1040
|
+
</p>
|
1041
|
+
<p align="center">
|
1042
|
+
<a href="https://www.clickfunnels.com?utm_source=hopsoft&utm_medium=open-source&utm_campaign=universalid">
|
1043
|
+
<img src="https://images.clickfunnel.com/uploads/digital_asset/file/176632/clickfunnels-dark-logo.svg" width="575" />
|
1044
|
+
</a>
|
1045
|
+
</p>
|
1046
|
+
|
1047
|
+
[Add your company...](https://github.com/sponsors/hopsoft/sponsorships?sponsor=hopsoft&tier_id=23918&preview=false)
|
275
1048
|
|
276
1049
|
## License
|
277
1050
|
|