universalid 0.1.3 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +387 -1094
- data/Rakefile +16 -2
- data/config/default.yml +1 -1
- data/config/example.yml +2 -2
- data/lib/universalid/encoder.rb +21 -0
- data/lib/universalid/extensions/active_record/base_message_pack_type.rb +15 -0
- data/lib/universalid/extensions/active_record/base_packer.rb +170 -0
- data/lib/universalid/extensions/active_record/base_unpacker.rb +61 -0
- data/lib/universalid/extensions/active_record/relation_message_pack_type.rb +20 -0
- data/lib/universalid/extensions/active_support/time_with_zone_message_pack_type.rb +18 -0
- data/lib/universalid/extensions/global_id/global_id_model.rb +28 -0
- data/lib/universalid/extensions/global_id/global_id_uid_extension.rb +43 -0
- data/lib/universalid/extensions/global_id/message_pack_type.rb +16 -0
- data/lib/universalid/extensions/signed_global_id/message_pack_type.rb +12 -0
- data/lib/{universal_id → universalid}/message_pack_types/ruby/composites/open_struct.rb +2 -0
- data/lib/universalid/message_pack_types/ruby/scalars/bigdecimal.rb +10 -0
- data/lib/{universal_id → universalid}/message_pack_types/ruby/scalars/date.rb +2 -0
- data/lib/{universal_id → universalid}/message_pack_types/ruby/scalars/date_time.rb +2 -0
- data/lib/{universal_id → universalid}/message_pack_types.rb +3 -5
- data/lib/universalid/packer.rb +19 -0
- data/lib/{universal_id → universalid}/prepack_database_options.rb +4 -4
- data/lib/{universal_id → universalid}/prepack_options.rb +2 -12
- data/lib/{universal_id → universalid}/prepacker.rb +3 -3
- data/lib/{universal_id → universalid}/refinements/open_struct_refinement.rb +2 -0
- data/lib/universalid/refinements.rb +3 -0
- data/lib/{universal_id → universalid}/settings.rb +3 -2
- data/lib/{universal_id → universalid}/version.rb +1 -1
- data/lib/universalid.rb +28 -1
- data/lib/uri/uid.rb +17 -7
- metadata +128 -50
- data/lib/universal_id/contrib/active_record/base_message_pack_type.rb +0 -11
- data/lib/universal_id/contrib/active_record/base_packer.rb +0 -130
- data/lib/universal_id/contrib/active_record/base_unpacker.rb +0 -50
- data/lib/universal_id/contrib/active_record/relation_message_pack_type.rb +0 -16
- data/lib/universal_id/contrib/active_record.rb +0 -4
- data/lib/universal_id/contrib/active_support/time_with_zone_message_pack_type.rb +0 -14
- data/lib/universal_id/contrib/active_support.rb +0 -3
- data/lib/universal_id/contrib/global_id/global_id_model.rb +0 -24
- data/lib/universal_id/contrib/global_id/global_id_uid_extension.rb +0 -36
- data/lib/universal_id/contrib/global_id/message_pack_type.rb +0 -15
- data/lib/universal_id/contrib/global_id.rb +0 -3
- data/lib/universal_id/contrib/rails.rb +0 -6
- data/lib/universal_id/contrib/signed_global_id/message_pack_type.rb +0 -8
- data/lib/universal_id/contrib/signed_global_id.rb +0 -3
- data/lib/universal_id/encoder.rb +0 -27
- data/lib/universal_id/refinements.rb +0 -8
- data/lib/universal_id.rb +0 -29
- /data/lib/{universal_id → universalid}/message_pack_factory.rb +0 -0
- /data/lib/{universal_id → universalid}/message_pack_types/ruby/composites/module.rb +0 -0
- /data/lib/{universal_id → universalid}/message_pack_types/ruby/composites/set.rb +0 -0
- /data/lib/{universal_id → universalid}/message_pack_types/ruby/composites/struct.rb +0 -0
- /data/lib/{universal_id → universalid}/message_pack_types/ruby/scalars/complex.rb +0 -0
- /data/lib/{universal_id → universalid}/message_pack_types/ruby/scalars/range.rb +0 -0
- /data/lib/{universal_id → universalid}/message_pack_types/ruby/scalars/rational.rb +0 -0
- /data/lib/{universal_id → universalid}/message_pack_types/ruby/scalars/regexp.rb +0 -0
- /data/lib/{universal_id → universalid}/message_pack_types/uri/uid/type.rb +0 -0
- /data/lib/{universal_id → universalid}/refinements/array_refinement.rb +0 -0
- /data/lib/{universal_id → universalid}/refinements/hash_refinement.rb +0 -0
- /data/lib/{universal_id → universalid}/refinements/set_refinement.rb +0 -0
data/README.md
CHANGED
@@ -1,200 +1,77 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
<
|
30
|
-
|
31
|
-
|
32
|
-
<
|
33
|
-
|
34
|
-
</a>
|
35
|
-
</p>
|
36
|
-
<h2 align="center">URL-Safe Portability for any Ruby Object</h2>
|
1
|
+
# Universal ID
|
2
|
+
|
3
|
+
<p>
|
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-795-47d299.svg" />
|
6
|
+
</a>
|
7
|
+
<a href="https://codeclimate.com/github/hopsoft/universalid/maintainability">
|
8
|
+
<img src="https://api.codeclimate.com/v1/badges/567624cbe733fafc2330/maintainability" />
|
9
|
+
</a>
|
10
|
+
<a href="https://rubygems.org/gems/universalid">
|
11
|
+
<img alt="GEM Version" src="https://img.shields.io/gem/v/universalid?color=168AFE&include_prereleases&logo=ruby&logoColor=FE1616">
|
12
|
+
</a>
|
13
|
+
<a href="https://rubygems.org/gems/universalid">
|
14
|
+
<img alt="GEM Downloads" src="https://img.shields.io/gem/dt/universalid?color=168AFE&logo=ruby&logoColor=FE1616">
|
15
|
+
</a>
|
16
|
+
<a href="https://github.com/testdouble/standard">
|
17
|
+
<img alt="Ruby Style" src="https://img.shields.io/badge/style-standard-168AFE?logo=ruby&logoColor=FE1616" />
|
18
|
+
</a>
|
19
|
+
<a href="https://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>
|
22
|
+
<a href="https://github.com/hopsoft/universalid/actions/workflows/tests.yml">
|
23
|
+
<img alt="Tests" src="https://github.com/hopsoft/universalid/actions/workflows/tests.yml/badge.svg" />
|
24
|
+
</a>
|
25
|
+
<a href="https://github.com/sponsors/hopsoft">
|
26
|
+
<img alt="Sponsors" src="https://img.shields.io/github/sponsors/hopsoft?color=eb4aaa&logo=GitHub%20Sponsors" />
|
27
|
+
</a>
|
28
|
+
<a href="https://ruby.social/@hopsoft">
|
29
|
+
<img alt="Ruby.Social Follow" src="https://img.shields.io/mastodon/follow/000008274?domain=https%3A%2F%2Fruby.social&label=%40hopsoft&style=social">
|
30
|
+
</a>
|
31
|
+
<a href="https://twitter.com/hopsoft">
|
32
|
+
<img alt="Twitter Follow" src="https://img.shields.io/twitter/url?label=%40hopsoft&style=social&url=https%3A%2F%2Ftwitter.com%2Fhopsoft">
|
33
|
+
</a>
|
37
34
|
</p>
|
38
35
|
|
39
|
-
|
40
|
-
|
36
|
+
## Fast, recursive, optimized, URL-Safe serialization for any Ruby object
|
37
|
+
|
38
|
+
Universal ID leverages both [MessagePack](https://msgpack.org/) and [Brotli](https://github.com/google/brotli) _(a combo built for speed and best-in-class data compression)_.
|
39
|
+
When combined, these libraries are up to 30% faster and within 2-5% compression rates compared to Protobuf. <a title="Source" href="https://g.co/bard/share/e5bdb17aee91">↗</a>
|
41
40
|
|
42
|
-
|
43
|
-
|
41
|
+
Universal ID opens the flood gates with a deluge of powerful yet easily implemented [**solutions** ↗](docs/use_cases.md) across a variety of problem domains.
|
42
|
+
|
43
|
+
> [!TIP]
|
44
|
+
> All the code examples below can be tested on your local machine. Just clone the repo _(↑or use Gitpod above↑)_ and run `bin/console` to begin exploring.
|
45
|
+
> Don't forget to execute `bundle` first to ensure all dependencies are up to date. **Happy coding!**
|
44
46
|
|
45
47
|
<!-- Tocer[start]: Auto-generated, don't remove. -->
|
46
48
|
|
47
49
|
## Table of Contents
|
48
50
|
|
49
|
-
- [Example Use Cases](#example-use-cases)
|
50
51
|
- [Supported Data Types](#supported-data-types)
|
51
|
-
- [
|
52
|
-
- [
|
52
|
+
- [Primitive Types](#primitive-types)
|
53
|
+
- [Composite Types](#composite-types)
|
54
|
+
- [Extension Types](#extension-types)
|
53
55
|
- [Custom Types](#custom-types)
|
54
|
-
|
55
|
-
- [
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
- [Performance and Benchmarks](#performance-and-benchmarks)
|
56
|
+
- [Options](#options)
|
57
|
+
- [Advanced Usage](#advanced-usage)
|
58
|
+
- [Fingerprinting](#fingerprinting)
|
59
|
+
- [Copy ActiveRecord Models](#copy-activerecord-models)
|
60
|
+
- [ActiveRecord::Relations](#activerecordrelations)
|
61
|
+
- [SignedGlobalID](#signedglobalid)
|
61
62
|
- [Sponsors](#sponsors)
|
62
63
|
- [License](#license)
|
63
64
|
|
64
65
|
<!-- Tocer[finish]: Auto-generated, don't remove. -->
|
65
66
|
|
66
|
-
## Example Use Cases
|
67
|
-
|
68
|
-
Universal ID's powerful serialization capabilities unlock a myriad of possibilities across various domains.
|
69
|
-
Here are a few possibilities.
|
70
|
-
|
71
|
-
- **State Management for Web Applications**:
|
72
|
-
Facilitate seamless user experiences in web applications by preserving and transferring UI states, even across different sessions.
|
73
|
-
|
74
|
-
- **Data Serialization for Distributed Systems**:
|
75
|
-
Enable efficient communication in distributed systems by serializing complex data structures for network transmission.
|
76
|
-
|
77
|
-
- **Configuration Settings for Software Applications**:
|
78
|
-
Store and manage configuration settings for software applications, allowing easy transfer and versioning of settings across installations.
|
79
|
-
|
80
|
-
- **Session Continuity in Cloud Services**:
|
81
|
-
Ensure continuity of user sessions in cloud-based applications, enabling users to pick up their work exactly where they left off, regardless of the device or location.
|
82
|
-
|
83
|
-
- **Audit Logging for Complex Transactions**:
|
84
|
-
Record detailed states of complex transactions in audit logs, providing a comprehensive and reversible record of actions for compliance and analysis.
|
85
|
-
|
86
|
-
- **Machine-to-Machine Communication**:
|
87
|
-
Standardize data formats for machine-to-machine communication, facilitating interoperability and data exchange in IoT and other automated systems.
|
88
|
-
|
89
|
-
These use cases demonstrate the versatility and power of Universal ID in various application and business scenarios, offering solutions that enhance efficiency, user experience, and system reliability.
|
90
|
-
|
91
|
-
<details>
|
92
|
-
<summary><b>See More Use Cases</b>... ▾</summary>
|
93
|
-
<p></p>
|
94
|
-
|
95
|
-
Harnessing the capabilities of this advanced low-level tool in your libraries and applications might initially seem daunting.
|
96
|
-
To assist you in unlocking its full potential, here are some additional suggestions to help you get started.
|
97
|
-
|
98
|
-
- **API Response Caching**
|
99
|
-
Cache complex API responses as serialized strings, allowing for efficient storage and quick retrieval.
|
100
|
-
|
101
|
-
- **Asset Management in Enterprises**
|
102
|
-
Serialize asset information, including status and location, for efficient tracking and management.
|
103
|
-
|
104
|
-
- **Audit Logging for Financial Transactions**
|
105
|
-
Serialize transaction states for audit trails in financial applications, providing detailed and reversible records for compliance.
|
106
|
-
|
107
|
-
- **Automated Testing of Web Applications**
|
108
|
-
Serialize application states to reproduce and test various scenarios automatically.
|
109
|
-
|
110
|
-
- **Backup and Restore of Application States**
|
111
|
-
Create snapshots of application states that can be backed up and later restored.
|
112
|
-
|
113
|
-
- **Configuration Management in DevOps**
|
114
|
-
Serialize configuration settings for software deployments, enabling easy versioning and rollback.
|
115
|
-
|
116
|
-
- **Content Management Systems (CMS)**
|
117
|
-
Serialize page or post states in CMS, enabling advanced versioning and preview functionalities.
|
118
|
-
|
119
|
-
- **Customer Support Tools**
|
120
|
-
Serialize user issues and their context, helping support teams to quickly understand and resolve customer problems.
|
121
|
-
|
122
|
-
- **Data Migration Between Databases**
|
123
|
-
Serialize entire database records for easy transfer between different database systems or formats.
|
124
|
-
|
125
|
-
- **Educational Platforms**
|
126
|
-
Serialize user progress and states in educational platforms, allowing students to pause and resume their learning activities.
|
127
|
-
|
128
|
-
- **E-commerce Cart Persistence**
|
129
|
-
Serialize shopping cart contents, enabling users to return to a filled cart even after their session expires.
|
130
|
-
|
131
|
-
- **Energy Management Systems**
|
132
|
-
Serialize energy usage data from various sensors for analysis and monitoring.
|
133
|
-
|
134
|
-
- **Environmental Monitoring Systems**
|
135
|
-
Serialize sensor data from environmental monitoring systems for analysis and historical record keeping.
|
136
|
-
|
137
|
-
- **Event Sourcing in Applications**
|
138
|
-
Use serialized states for event sourcing, maintaining an immutable log of state changes over time.
|
139
|
-
|
140
|
-
- **Gaming State Preservation**
|
141
|
-
Store game states as serialized strings, allowing players to resume games exactly where they left off.
|
142
|
-
|
143
|
-
- **Healthcare Data Exchange**
|
144
|
-
Securely transfer patient data between different healthcare systems while maintaining the integrity of complex data structures.
|
145
|
-
|
146
|
-
- **IoT Device State Management**
|
147
|
-
Serialize the state of IoT devices for efficient transmission over networks, aiding in remote monitoring and control.
|
148
|
-
|
149
|
-
- **Legal Document Management**
|
150
|
-
Serialize versions of legal documents, maintaining a trail of edits and changes for auditing.
|
151
|
-
|
152
|
-
- **Machine Learning Data Preparation**
|
153
|
-
Serialize complex data structures used in machine learning pipelines for efficient processing.
|
154
|
-
|
155
|
-
- **Microservice Communication**
|
156
|
-
Serialize complex objects for inter-service communication, ensuring efficient data transfer and reducing the need for complex parsing logic.
|
157
|
-
|
158
|
-
- **Real Estate Portfolio Management**
|
159
|
-
Serialize complex property data for portfolio management and analysis.
|
160
|
-
|
161
|
-
- **Real-time Collaboration Tools**
|
162
|
-
Serialize document or application states for real-time collaboration tools, ensuring consistency across different user sessions.
|
163
|
-
|
164
|
-
- **Research Data Management**
|
165
|
-
Serialize research data and experimental setups for ease of sharing and replication of experiments.
|
166
|
-
|
167
|
-
- **Retail Inventory Management**
|
168
|
-
Serialize inventory data, including details of products, for efficient management and tracking.
|
169
|
-
|
170
|
-
- **Session Continuity Across Devices**
|
171
|
-
Store user session data as a serialized string, enabling users to resume their session on a different device without loss of context.
|
172
|
-
|
173
|
-
- **State Management for Single Page Applications (SPAs)**
|
174
|
-
Serialize UI states into URL-safe strings, enabling bookmarking or sharing of specific application states.
|
175
|
-
|
176
|
-
- **Supply Chain Logistics**
|
177
|
-
Serialize complex logistics and shipment data, aiding in efficient tracking and management.
|
178
|
-
|
179
|
-
- **Telecommunication Network Management**
|
180
|
-
Serialize configurations and states of network devices for efficient management and troubleshooting.
|
181
|
-
|
182
|
-
- **Travel Itinerary Planning**
|
183
|
-
Serialize travel plans and itineraries, allowing users to save and share their travel details easily.
|
184
|
-
|
185
|
-
- **Version Control of Design Files**
|
186
|
-
Serialize design artifacts for version control in graphic design and CAD applications.
|
187
|
-
</details>
|
188
|
-
|
189
|
-
> :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!
|
190
|
-
|
191
67
|
## Supported Data Types
|
192
68
|
|
193
|
-
###
|
69
|
+
### Primitive Types
|
194
70
|
|
195
|
-
Universal ID supports most Ruby primitives
|
71
|
+
Universal ID supports most native Ruby primitives:
|
196
72
|
|
197
73
|
- `NilClass`
|
74
|
+
- `BigDecimal`
|
198
75
|
- `Complex`
|
199
76
|
- `Date`
|
200
77
|
- `DateTime`
|
@@ -209,1028 +86,444 @@ Universal ID supports most Ruby primitives.
|
|
209
86
|
- `Time`
|
210
87
|
- `TrueClass`
|
211
88
|
|
212
|
-
You can use Universal ID
|
213
|
-
_See below..._
|
89
|
+
You can use Universal ID to serialize individual primitives, but this actually serves as the foundation for more advanced use-cases.
|
214
90
|
|
215
91
|
```ruby
|
216
92
|
uri = URI::UID.build(:demo).to_s
|
217
|
-
#=> "uid://
|
93
|
+
#=> "uid://universalid/iwKA1gBkZW1vAw#CwWAkccHf6ZTeW1ib2wD"
|
218
94
|
|
219
95
|
uid = URI::UID.parse(uri)
|
220
|
-
#=> #<URI::UID
|
96
|
+
#=> #<URI::UID payload=iwKA1gBkZW1vAw, fingerprint=CwWAkccHf6ZTeW1ib2wD>
|
221
97
|
|
222
98
|
uid.decode
|
223
99
|
#=> :demo
|
224
100
|
```
|
225
101
|
|
226
|
-
###
|
227
|
-
|
228
|
-
Composite support is where things start to get interesting. All of the composite datatypes listed below can be recursively transformed into a Universal ID.
|
229
|
-
|
230
|
-
<details>
|
231
|
-
<summary><b><code>[]</code> Array</b>... ▾</summary>
|
232
|
-
<p></p>
|
233
|
-
|
234
|
-
```ruby
|
235
|
-
array = [1, 2, 3, [:a, :b, :c, [true]]]
|
236
|
-
|
237
|
-
uri = URI::UID.build(array).to_s
|
238
|
-
#=> "uid://universal-id/iweAlAECA5TUAGHUAGLUAGORwwM"
|
239
|
-
|
240
|
-
uid = URI::UID.parse(uri)
|
241
|
-
#=> #<URI::UID uid://universal-id/iweAlAECA5TUAGHUAGLUAGORwwM>
|
242
|
-
|
243
|
-
uid.decode
|
244
|
-
#=> [1, 2, 3, [:a, :b, :c, [true]]]
|
245
|
-
```
|
246
|
-
</details>
|
247
|
-
|
248
|
-
<details>
|
249
|
-
<summary><b><code>{}</code> Hash</b>... ▾</summary>
|
250
|
-
<p></p>
|
251
|
-
|
252
|
-
```ruby
|
253
|
-
hash = {a: 1, b: 2, c: 3, array: [1, 2, 3, [:a, :b, :c, [true]]]}
|
102
|
+
### Composite Types
|
254
103
|
|
255
|
-
|
256
|
-
|
104
|
+
Composite _(or complex, compound, etc.)_ datatype support is where things start to get interesting.
|
105
|
+
Universal ID supports the following native Ruby composite datatypes:
|
257
106
|
|
258
|
-
|
259
|
-
|
107
|
+
- `Array`
|
108
|
+
- `Hash`
|
109
|
+
- `OpenStruct`
|
110
|
+
- `Set`
|
111
|
+
- `Struct`
|
260
112
|
|
261
|
-
|
262
|
-
|
263
|
-
```
|
264
|
-
</details>
|
265
|
-
|
266
|
-
<details>
|
267
|
-
<summary><b><code><></code> Open Struct</b>... ▾</summary>
|
268
|
-
<p></p>
|
269
|
-
|
270
|
-
```ruby
|
271
|
-
ostruct = OpenStruct.new(
|
272
|
-
name: "Wireless Keyboard",
|
273
|
-
price: 49.99,
|
274
|
-
category: "Electronics",
|
275
|
-
in_stock: true
|
276
|
-
)
|
277
|
-
|
278
|
-
uri = URI::UID.build(ostruct).to_s
|
279
|
-
#=> "uid://universal-id/iyaAx0sMhNYAbmFtZbFXaXJlbGVzcyBLZXlib2FyZMcFAHByaWNly0BI_rhR64Uf1wBjYXRlZ29ye..."
|
280
|
-
|
281
|
-
uid = URI::UID.parse(uri)
|
282
|
-
#=> #<URI::UID scheme=uid, host=universal-id, payload=iyaAx0sMhNYAbmFtZbFXaXJlbGVzcyBLZXlib2FyZMcFAHByaWNly0BI_rhR64Uf1wBjYXRlZ29ye...>
|
283
|
-
|
284
|
-
uid.decode
|
285
|
-
#=> #<OpenStruct name="Wireless Keyboard", price=49.99, category="Electronics", in_stock=true>
|
286
|
-
```
|
287
|
-
</details>
|
288
|
-
|
289
|
-
<details>
|
290
|
-
<summary><b><code>()</code> Set</b>... ▾</summary>
|
291
|
-
<p></p>
|
292
|
-
|
293
|
-
```ruby
|
294
|
-
set = Set.new([1, 2, 3, [:a, :b, :c, [true]]])
|
113
|
+
```ruby
|
114
|
+
array = [1, 2, 3, [:a, :b, :c, [true]]]
|
295
115
|
|
296
|
-
|
297
|
-
|
116
|
+
uri = URI::UID.build(array).to_s
|
117
|
+
#=> "uid://universalid/iweAlAECA5TUAGHUAGLUAGORwwM#iwSAkccGf6VBcnJheQM"
|
298
118
|
|
299
|
-
|
300
|
-
|
119
|
+
uid = URI::UID.parse(uri)
|
120
|
+
#=> #<URI::UID payload=iweAlAECA5TUAGHUAGLUAGORwwM, fingerprint=iwSAkccGf6VBcnJheQM>
|
301
121
|
|
302
|
-
|
303
|
-
|
304
|
-
```
|
305
|
-
</details>
|
122
|
+
uid.decode
|
123
|
+
#=> [1, 2, 3, [:a, :b, :c, [true]]]
|
306
124
|
|
307
|
-
|
308
|
-
|
309
|
-
|
125
|
+
uid.decode == array
|
126
|
+
#=> true
|
127
|
+
```
|
310
128
|
|
311
|
-
|
312
|
-
|
313
|
-
book = Book.new("The Great Gatsby", "F. Scott Fitzgerald", "9780743273565", 1925)
|
129
|
+
```ruby
|
130
|
+
hash = {a: 1, b: 2, c: 3, array: [1, 2, 3, [:a, :b, :c, [true]]]}
|
314
131
|
|
315
|
-
|
316
|
-
|
132
|
+
uri = URI::UID.build(hash).to_s
|
133
|
+
#=> "uid://universalid/CxKAhNQAYQHUAGIC1ABjA8cFAGFycmF5lAECA5TUAGHUAGLUAGORwwM#CwS..."
|
317
134
|
|
318
|
-
|
319
|
-
|
135
|
+
uid = URI::UID.parse(uri)
|
136
|
+
#=> #<URI::UID payload=CxKAhNQAYQHUAGIC1ABjA8cFAGFycmF5lAECA..., fingerprint=CwSAkccFf6RIYXNoAw>
|
320
137
|
|
321
|
-
|
322
|
-
|
323
|
-
```
|
324
|
-
</details>
|
138
|
+
uid.decode
|
139
|
+
#=> {:a=>1, :b=>2, :c=>3, :array=>[1, 2, 3, [:a, :b, :c, [true]]]}
|
325
140
|
|
326
|
-
|
141
|
+
uid.decode == hash
|
142
|
+
#=> true
|
143
|
+
```
|
327
144
|
|
328
|
-
|
329
|
-
|
145
|
+
```ruby
|
146
|
+
Book = Struct.new(:title, :author, :isbn, :published_year)
|
147
|
+
book = Book.new("The Great Gatsby", "F. Scott Fitzgerald", "9780743273565", 1925)
|
330
148
|
|
331
|
-
|
332
|
-
|
333
|
-
<p></p>
|
149
|
+
uri = URI::UID.build(book).to_s
|
150
|
+
#=> "uid://universalid/G2YAoGTomv9tT1ilLRgVC9vIpmuBo-k84FZ0G8-siFMBNsbW0dpBE0Tnm96..."
|
334
151
|
|
335
|
-
|
336
|
-
|
337
|
-
attr_accessor :user_id, :preferences
|
152
|
+
uid = URI::UID.parse(uri)
|
153
|
+
#=> #<URI::UID payload=G2YAoGTomv9tT1ilLRgVC9vIpmuBo-k84FZ0G..., fingerprint=CwSAkccFf6RCb29rAw>
|
338
154
|
|
339
|
-
|
340
|
-
|
341
|
-
@preferences = preferences
|
342
|
-
end
|
343
|
-
end
|
155
|
+
uid.decode
|
156
|
+
#=> #<struct Book title="The Great Gatsby", author="F. Scott Fitzgerald", isbn="9780743273565", published_year=1925>
|
344
157
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
packer.write user_preferences.user_id
|
349
|
-
packer.write user_preferences.preferences
|
350
|
-
end,
|
351
|
-
unpacker: ->(unpacker) do
|
352
|
-
user_id = unpacker.read
|
353
|
-
preferences = unpacker.read
|
354
|
-
UserSettings.new user_id, preferences
|
355
|
-
end
|
356
|
-
)
|
158
|
+
uid.decode == book
|
159
|
+
#=> true
|
160
|
+
```
|
357
161
|
|
358
|
-
|
359
|
-
theme: "dark",
|
360
|
-
notifications: "email",
|
361
|
-
language: "en",
|
362
|
-
layout: "grid",
|
363
|
-
privacy: "private"
|
364
|
-
)
|
162
|
+
### Extension Types
|
365
163
|
|
366
|
-
|
367
|
-
#=> "uid://universal-id/G1QAQAT-bfcGW1QOgadJwJF06yL8gDnGgfs1Xdti20TDDvG5STPqzbYcQ6TBqVKhdZ39CdQZUwEGe..."
|
164
|
+
The following extension datatypes ship with Universal ID:
|
368
165
|
|
369
|
-
|
370
|
-
|
166
|
+
- `ActiveRecord::Base`
|
167
|
+
- `ActiveRecord::Relation`
|
168
|
+
- `ActiveSupport::TimeWithZone`
|
169
|
+
- `GlobalID`
|
170
|
+
- `SignedGlobalID`
|
371
171
|
|
372
|
-
|
373
|
-
|
374
|
-
```
|
375
|
-
</details>
|
172
|
+
> [!NOTE]
|
173
|
+
> Extensions are autoloaded whenever the related datatype is detected.
|
376
174
|
|
377
|
-
|
175
|
+
> [!IMPORTANT]
|
176
|
+
> **Why Universal ID with ActiveRecord?**
|
177
|
+
> ActiveRecord already has GlobalID, a robust library for serializing individual models.
|
178
|
+
> **Universal ID covers a much wider range of use cases**.
|
378
179
|
|
379
|
-
|
380
|
-
These contributions can introduce support for additional data types, further broadening the scope of Universal ID’s utility.
|
381
|
-
The following are some notable contrib extensions:
|
180
|
+
Here are a few reasons you may want to consider Universal ID with ActiveRecord.
|
382
181
|
|
383
|
-
- **
|
384
|
-
|
182
|
+
- **New Records**:
|
183
|
+
Universal ID can serialize models that haven't been saved to the database yet.
|
385
184
|
|
386
|
-
- **
|
387
|
-
|
185
|
+
- **Changesets**:
|
186
|
+
Universal ID can serialize ActiveRecord models with unsaved changes, ensuring that even transient states are captured.
|
388
187
|
|
389
|
-
- **
|
390
|
-
|
188
|
+
- **Associations**:
|
189
|
+
Universal ID goes beyond single models. It can include associated records, even those with unsaved changes, creating a comprehensive snapshot of complex record states.
|
391
190
|
|
392
|
-
- **
|
393
|
-
|
191
|
+
- **Copying/Cloning**:
|
192
|
+
Universal ID supports making copies of records _(including associations)_, making it ideal for duplicating complex datasets.
|
394
193
|
|
395
|
-
- **
|
396
|
-
|
194
|
+
- **More Control**:
|
195
|
+
Universal ID gives you control over the serialization process. You can choose which columns to include/exclude, allowing for tailored, optimized payloads to fit your needs.
|
397
196
|
|
398
|
-
|
197
|
+
- **Queries/Relations**:
|
198
|
+
Universal ID also supports ActiveRecord::Relations, enabling the serialization of complex database queries and scopes.
|
399
199
|
|
400
|
-
|
401
|
-
This ensures the extensions are loaded and available for use.
|
402
|
-
Here is an example illustrating how to include contributed types:
|
200
|
+
In summary, while GlobalID excels in its specific use case, Universal ID offers more power for use-cases that involve unsaved records, complex associations, data cloning, and database queries.
|
403
201
|
|
404
202
|
```ruby
|
405
|
-
#
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
require "universal_id/contrib/signed_global_id"
|
410
|
-
|
411
|
-
# or simply
|
412
|
-
require "universal_id/contrib/rails"
|
413
|
-
```
|
414
|
-
|
415
|
-
> :bulb: **Implicit Contribs**: Whenever the `Rails` constant is defined, the related contribs are auto-loaded.
|
416
|
-
|
417
|
-
> :bulb: **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**.
|
418
|
-
|
419
|
-
**Why Universal ID with ActiveRecord?**
|
420
|
-
|
421
|
-
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.
|
422
|
-
|
423
|
-
- **Support for New Records**:
|
424
|
-
Unlike GlobalID, Universal ID can serialize models that haven't been saved to the database yet.
|
425
|
-
|
426
|
-
- **Capturing Unsaved Changes**:
|
427
|
-
It can serialize ActiveRecord models with unsaved changes, ensuring that even transient states are captured.
|
203
|
+
# setup some records
|
204
|
+
campaign = Campaign.create(name: "My Campaign")
|
205
|
+
email = campaign.emails.create(subject: "First Email")
|
206
|
+
attachment = email.attachments.create(file_name: "data.pdf")
|
428
207
|
|
429
|
-
|
430
|
-
|
208
|
+
# ensure associations are loaded so they can be included in an UID
|
209
|
+
campaign.emails.load
|
210
|
+
campaign.emails.each { |e| e.attachments.load }
|
431
211
|
|
432
|
-
|
433
|
-
|
212
|
+
# make some unsaved changes
|
213
|
+
email.subject = "1st Email"
|
434
214
|
|
435
|
-
|
436
|
-
|
215
|
+
# add an unsaved record
|
216
|
+
campaign.emails.build(subject: "2nd Email")
|
437
217
|
|
438
|
-
|
439
|
-
|
218
|
+
# introspection
|
219
|
+
campaign.emails.size #=> 2
|
220
|
+
campaign.emails.loaded? #=> true
|
221
|
+
campaign.emails.last.new_record? #=> true
|
440
222
|
|
441
|
-
|
223
|
+
options = {
|
224
|
+
include_changes: true,
|
225
|
+
include_descendants: true,
|
226
|
+
descendant_depth: 2
|
227
|
+
}
|
442
228
|
|
443
|
-
|
444
|
-
|
445
|
-
<p></p>
|
229
|
+
uri = URI::UID.build(campaign, options).to_s
|
230
|
+
#=> "uid://universalid/GxYBYGT6_Xn_OrelIDRWhQQgvbS5gQxV7EJKe3paIiEFmEEc1gLKw8Pl2-k..."
|
446
231
|
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
#=> "uid://universal-id/CwiAxw4EqENhbXBhaWdugaJpZAMD"
|
466
|
-
|
467
|
-
uid = URI::UID.parse(uri)
|
468
|
-
#=> #<URI::UID uid://universal-id/CwiAxw4EqENhbXBhaWdugaJpZAMD>
|
469
|
-
|
470
|
-
URI::UID.parse(uri).decode
|
471
|
-
##<Campaign:0x000000011cc67da8 id: 1, name: "Marketing Campaign", ...>
|
472
|
-
```
|
473
|
-
</details>
|
474
|
-
|
475
|
-
## Settings and Prepack Options
|
476
|
-
|
477
|
-
Universal ID supports a small but powerful set of configuration options for transforming objects before being
|
478
|
-
handed off to MessagePack for serialization.
|
479
|
-
|
480
|
-
Prepacking gives you explicit control over what data to include in the Universal ID.
|
481
|
-
|
482
|
-
|
483
|
-
<details>
|
484
|
-
<summary><b>View All Settings and Prepack Options</b>... ▾</summary>
|
485
|
-
<p></p>
|
486
|
-
|
487
|
-
```yml
|
488
|
-
prepack:
|
489
|
-
# ..........................................................................................................
|
490
|
-
# A list of attributes to exclude (for objects like Hash, OpenStruct, Struct, etc.)
|
491
|
-
# Takes prescedence over the`include` list
|
492
|
-
exclude: []
|
493
|
-
|
494
|
-
# ..........................................................................................................
|
495
|
-
# A list of attributes to include (for objects like Hash, OpenStruct, Struct, etc.)
|
496
|
-
include: []
|
497
|
-
|
498
|
-
# ..........................................................................................................
|
499
|
-
# Whether or not to omit blank values when packing (nil, {}, [], "", etc.)
|
500
|
-
include_blank: true
|
501
|
-
|
502
|
-
# ==========================================================================================================
|
503
|
-
# Database records
|
504
|
-
database:
|
505
|
-
# ......................................................................................................
|
506
|
-
# Whether or not to include primary/foreign keys
|
507
|
-
# Setting this to `false` can be used to make a copy of an existing record
|
508
|
-
include_keys: true
|
509
|
-
|
510
|
-
# ......................................................................................................
|
511
|
-
# Whether or not to include date/time timestamps (created_at, updated_at, etc.)
|
512
|
-
# Setting this to `false` can be used to make a copy of an existing record
|
513
|
-
include_timestamps: true
|
514
|
-
|
515
|
-
# ......................................................................................................
|
516
|
-
# Whether or not to include unsaved changes
|
517
|
-
# Assign to `true` when packing new records
|
518
|
-
include_unsaved_changes: false
|
519
|
-
|
520
|
-
# ......................................................................................................
|
521
|
-
# Whether or not to include loaded in-memory descendants (i.e. child associations)
|
522
|
-
include_descendants: false
|
523
|
-
|
524
|
-
# ......................................................................................................
|
525
|
-
# The max depth (number) of loaded in-memory descendants to include when `include_descendants == true`
|
526
|
-
# For example, a value of (3) would include the following:
|
527
|
-
# Parent > Child > Grandchild
|
528
|
-
descendant_depth: 0
|
529
|
-
```
|
530
|
-
</details>
|
531
|
-
|
532
|
-
Prepack options can be applied when creating a Universal ID and can be passed in structured or flat format.
|
533
|
-
|
534
|
-
<details>
|
535
|
-
<summary><b>How to Apply Prepack Options when Creating UIDs</b>... ▾</summary>
|
536
|
-
<p></p>
|
537
|
-
|
538
|
-
```ruby
|
539
|
-
person = {
|
540
|
-
full_name: "Jane Doe",
|
541
|
-
email: "janedoe@example.com",
|
542
|
-
birthdate: "1980-05-15",
|
543
|
-
phone_number: "555-6789",
|
544
|
-
ssn: "123-45-6789",
|
545
|
-
children: [
|
546
|
-
{
|
547
|
-
full_name: "Alice Doe",
|
548
|
-
email: "alicedoe@example.com",
|
549
|
-
birthdate: nil,
|
550
|
-
phone_number: "555-1234",
|
551
|
-
ssn: "987-65-4321"
|
552
|
-
},
|
553
|
-
{
|
554
|
-
full_name: "Bob Doe",
|
555
|
-
email: "bobdoe@example.com",
|
556
|
-
birthdate: "2008-11-21",
|
557
|
-
phone_number: nil,
|
558
|
-
ssn: "456-12-1234"
|
559
|
-
}
|
560
|
-
]
|
561
|
-
}
|
562
|
-
|
563
|
-
uid = URI::UID.build(person, include_blank: false, exclude: [:phone_number, :ssn])
|
564
|
-
uid.decode
|
565
|
-
|
566
|
-
# Note that the decoded payload is smaller due to the prepack options
|
567
|
-
# Also note that the options were applied recursively
|
568
|
-
|
569
|
-
{
|
570
|
-
full_name: "Jane Doe",
|
571
|
-
email: "janedoe@example.com",
|
572
|
-
birthdate: "1980-05-15",
|
573
|
-
children: [
|
574
|
-
{
|
575
|
-
full_name: "Alice Doe",
|
576
|
-
email: "alicedoe@example.com"
|
577
|
-
},
|
578
|
-
{
|
579
|
-
full_name: "Bob Doe",
|
580
|
-
email: "bobdoe@example.com",
|
581
|
-
birthdate: "2008-11-21"
|
582
|
-
}
|
583
|
-
]
|
584
|
-
}
|
585
|
-
```
|
586
|
-
</details>
|
587
|
-
|
588
|
-
It's also possible to register frequently used options as reusable settings to further simplify creating UIDs.
|
589
|
-
|
590
|
-
<details>
|
591
|
-
<summary><b>How to Register Prepack Options as Preconfigured Settings</b>... ▾</summary>
|
592
|
-
<p></p>
|
593
|
-
|
594
|
-
```yaml
|
595
|
-
# app/config/unsaved.yml
|
596
|
-
prepack:
|
597
|
-
include_blank: false
|
598
|
-
|
599
|
-
database:
|
600
|
-
include_unsaved_changes: true
|
601
|
-
include_timestamps: false
|
602
|
-
```
|
603
|
-
|
604
|
-
```ruby
|
605
|
-
UniversalID::Settings.register :unsaved, File.expand_path("app/config/unsaved.yml", __dir__)
|
606
|
-
URI::UID.build @record, UniversalID::Settings[:small_record]
|
607
|
-
```
|
608
|
-
</details>
|
609
|
-
|
610
|
-
## Advanced ActiveRecord
|
611
|
-
|
612
|
-
Universal ID includes some advanced capabilities when used with ActiveRecord.
|
613
|
-
|
614
|
-
- [x] **Include loaded associations**
|
615
|
-
Universal ID supports including `loaded` associations when a model is transformed into a UID.
|
616
|
-
<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>
|
617
|
-
|
618
|
-
- [x] **Include unsaved changes**
|
619
|
-
Universal ID supports capturing unsaved change, for both new and persisted records, when a model is transformed into a UID.
|
620
|
-
<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>
|
621
|
-
|
622
|
-
- [x] **Exclude keys** to make copies of existing records
|
623
|
-
Universal ID supports making copies of individual records or entire collections by opt'ing to exclude keys when transorming to UID.
|
624
|
-
<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>
|
625
|
-
|
626
|
-
First, let's establish the schema structure and data we'll be working with.
|
627
|
-
We'll limit ourselves to 3 tables here, but Universal ID can support much more complex data models.
|
628
|
-
|
629
|
-
- Campaign
|
630
|
-
- Email
|
631
|
-
- Attachment
|
632
|
-
|
633
|
-
We'll use 1 campaign with 3 emails that have 2 attachments each.
|
634
|
-
|
635
|
-
<details>
|
636
|
-
<summary><b>Setup the Schema</b>... ▾</summary>
|
637
|
-
<p></p>
|
638
|
-
|
639
|
-
```ruby
|
640
|
-
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
641
|
-
|
642
|
-
ActiveRecord::Schema.define do
|
643
|
-
create_table :campaigns do |t|
|
644
|
-
t.column :name, :string
|
645
|
-
t.column :description, :text
|
646
|
-
t.column :trigger, :string
|
647
|
-
t.timestamps
|
648
|
-
end
|
649
|
-
|
650
|
-
create_table :emails do |t|
|
651
|
-
t.column :campaign_id, :integer
|
652
|
-
t.column :subject, :string
|
653
|
-
t.column :body, :text
|
654
|
-
t.column :wait, :integer
|
655
|
-
t.timestamps
|
656
|
-
end
|
657
|
-
|
658
|
-
create_table :attachments do |t|
|
659
|
-
t.column :email_id, :integer
|
660
|
-
t.column :file_name, :string
|
661
|
-
t.column :content_type, :string
|
662
|
-
t.column :file_size, :integer
|
663
|
-
t.column :file_data, :binary
|
664
|
-
t.timestamps
|
665
|
-
end
|
666
|
-
end
|
667
|
-
```
|
668
|
-
</details>
|
232
|
+
uid = URI::UID.parse(uri)
|
233
|
+
#=> #<URI::UID payload=GxYBYGT6_Xn_OrelIDRWhQQgvbS5gQxV7EJKe..., fingerprint=CwuAkscJf6hDYW1wYWlnbtf_ReuZnGWeG5MD>
|
234
|
+
|
235
|
+
decoded = uid.decode
|
236
|
+
#=> "#<Campaign id: 13, name: \"My Campaign\" ...>
|
237
|
+
|
238
|
+
decoded == campaign
|
239
|
+
#=> true
|
240
|
+
|
241
|
+
# introspection
|
242
|
+
decoded.emails.size #=> 2
|
243
|
+
decoded.emails.loaded? #=> true
|
244
|
+
decoded.emails.first.changed? #=> true
|
245
|
+
decoded.emails.first.changes #=> {"subject"=>["First Email", "1st Email"]}
|
246
|
+
decoded.emails.last.new_record? #=> true
|
247
|
+
decoded.save #=> true
|
248
|
+
decoded.emails.last.persisted? #=> true
|
249
|
+
```
|
669
250
|
|
670
|
-
|
671
|
-
<summary><b>Setup the Models</b>... ▾</summary>
|
672
|
-
<p></p>
|
251
|
+
### Custom Types
|
673
252
|
|
674
|
-
|
675
|
-
|
676
|
-
has_many :emails, dependent: :destroy
|
677
|
-
end
|
253
|
+
Universal ID is extensible, enabling you to register your own datatypes with custom serialization rules.
|
254
|
+
Simply convert the required data to a Ruby primitive or composite value.
|
678
255
|
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
256
|
+
```ruby
|
257
|
+
# create a custom type
|
258
|
+
class UserSettings
|
259
|
+
attr_accessor :user_id, :preferences
|
683
260
|
|
684
|
-
|
685
|
-
|
261
|
+
def initialize(user_id, preferences = {})
|
262
|
+
@user_id = user_id
|
263
|
+
@preferences = preferences
|
686
264
|
end
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
)
|
701
|
-
|
702
|
-
# NOTE: Assigning campaign.emails via `=` to ensure ActiveRecord flags the association as `loaded`
|
703
|
-
campaign.emails = 3.times.map do |i|
|
704
|
-
email = campaign.emails.build(
|
705
|
-
subject: "Summer Sale Special Offer #{i + 1}",
|
706
|
-
body: "Dear Customer, check out our exclusive summer sale offers! #{i + 1}",
|
707
|
-
wait: rand(1..14)
|
708
|
-
)
|
709
|
-
|
710
|
-
# NOTE: Assigning email.attachments via `=` to ensure ActiveRecord flags the association as `loaded`
|
711
|
-
email.tap do |e|
|
712
|
-
e.attachments = 2.times.map do |j|
|
713
|
-
data = SecureRandom.random_bytes(rand(500..1500))
|
714
|
-
e.attachments.build(
|
715
|
-
file_name: "summer_sale_#{i + 1}_attachment_#{j + 1}.pdf",
|
716
|
-
content_type: "application/pdf",
|
717
|
-
file_size: data.size,
|
718
|
-
file_data: data
|
719
|
-
)
|
720
|
-
end
|
721
|
-
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# register the custom type with Universal ID
|
268
|
+
UniversalID::MessagePackFactory.register(
|
269
|
+
type: UserSettings,
|
270
|
+
packer: ->(user_preferences, packer) do
|
271
|
+
packer.write user_preferences.user_id
|
272
|
+
packer.write user_preferences.preferences
|
273
|
+
end,
|
274
|
+
unpacker: ->(unpacker) do
|
275
|
+
user_id = unpacker.read
|
276
|
+
preferences = unpacker.read
|
277
|
+
UserSettings.new user_id, preferences
|
722
278
|
end
|
279
|
+
)
|
280
|
+
|
281
|
+
# create an instance of the custom type
|
282
|
+
settings = UserSettings.new(1,
|
283
|
+
theme: "dark",
|
284
|
+
notifications: "email",
|
285
|
+
language: "en",
|
286
|
+
layout: "grid",
|
287
|
+
privacy: "private"
|
288
|
+
)
|
289
|
+
|
290
|
+
# serialize the custom type
|
291
|
+
uri = URI::UID.build(settings).to_s
|
292
|
+
#=> "uid://universalid/G1QAQAT-c_cO7qJcAk-TtsAiadci_IA5xoH7NV3bYttEww7xuUkzasu2HEO..."
|
293
|
+
|
294
|
+
# deserialize the custom type
|
295
|
+
uid = URI::UID.parse(uri)
|
296
|
+
#=> #<URI::UID payload=G1QAQAT-c_cO7qJcAk-TtsAiadci_IA5xoH7N..., fingerprint=CwiAkccNf6xVc2VyU2V0dGluZ3MD>
|
723
297
|
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
campaign.new_record? # true
|
728
|
-
campaign.changed? # true
|
298
|
+
uid.decode
|
299
|
+
=> #<UserSettings:0x000000011d0deb20 @preferences={:theme=>"dark", :notifications=>"email", :language=>"en", :layout=>"grid", :privacy=>"private"}, @user_id=1>
|
300
|
+
```
|
729
301
|
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
302
|
+
## Options
|
303
|
+
|
304
|
+
Universal ID supports a small, but powerful, set of options used to "prepack" the object before it's packed with MessagePack.
|
305
|
+
These options instruct Universal ID on how to prepare the object for serialization.
|
306
|
+
|
307
|
+
```yml
|
308
|
+
prepack:
|
309
|
+
# ..........................................................................................................
|
310
|
+
# A list of attributes to exclude (for objects like Hash, OpenStruct, Struct, etc.)
|
311
|
+
# Takes prescedence over the`include` list
|
312
|
+
exclude: []
|
313
|
+
|
314
|
+
# ..........................................................................................................
|
315
|
+
# A list of attributes to include (for objects like Hash, OpenStruct, Struct, etc.)
|
316
|
+
include: []
|
317
|
+
|
318
|
+
# ..........................................................................................................
|
319
|
+
# Whether or not to include blank values when packing (nil, {}, [], "", etc.)
|
320
|
+
include_blank: true
|
321
|
+
|
322
|
+
# ==========================================================================================================
|
323
|
+
# Database records
|
324
|
+
database:
|
325
|
+
# ......................................................................................................
|
326
|
+
# Whether or not to include primary/foreign keys
|
327
|
+
# Setting this to `false` can be used to make a copy of an existing record
|
328
|
+
include_keys: true
|
329
|
+
|
330
|
+
# ......................................................................................................
|
331
|
+
# Whether or not to include date/time timestamps (created_at, updated_at, etc.)
|
332
|
+
# Setting this to `false` can be used to make a copy of an existing record
|
333
|
+
include_timestamps: true
|
334
|
+
|
335
|
+
# ......................................................................................................
|
336
|
+
# Whether or not to include unsaved changes
|
337
|
+
# Assign to `true` when packing new records
|
338
|
+
include_changes: false
|
339
|
+
|
340
|
+
# ......................................................................................................
|
341
|
+
# Whether or not to include loaded in-memory descendants (i.e. child associations)
|
342
|
+
include_descendants: false
|
343
|
+
|
344
|
+
# ......................................................................................................
|
345
|
+
# The max depth (number) of loaded in-memory descendants to include when `include_descendants == true`
|
346
|
+
# For example, a value of (2) would include the following:
|
347
|
+
# Parent > Child > Grandchild
|
348
|
+
descendant_depth: 0
|
349
|
+
```
|
734
350
|
|
735
|
-
|
736
|
-
attachment.new_record? # true
|
737
|
-
attachment.changed? # true
|
738
|
-
end
|
739
|
-
end
|
740
|
-
```
|
741
|
-
</details>
|
351
|
+
Options can be applied whenever creating a UID.
|
742
352
|
|
743
|
-
|
353
|
+
```ruby
|
354
|
+
hash = { a: 1, b: 2, c: 3 }
|
744
355
|
|
745
|
-
|
746
|
-
|
747
|
-
<p></p>
|
356
|
+
uri = URI::UID.build(hash, exclude: [:b]).to_s
|
357
|
+
#=> "uid://universalid/CwSAgtQAYQHUAGMDAw#CwSAkccFf6RIYXNoAw"
|
748
358
|
|
749
|
-
|
750
|
-
|
751
|
-
options = {
|
752
|
-
include_unsaved_changes: true,
|
753
|
-
include_descendants: true,
|
754
|
-
descendant_depth: 2
|
755
|
-
}
|
756
|
-
|
757
|
-
# NOTE: The campaign model instance was setup earlier in the "Model Instances" section above
|
758
|
-
campaign.new_record? # true
|
759
|
-
campaign.changes
|
760
|
-
# {
|
761
|
-
# "name"=>[nil, "Summer Sale Campaign"],
|
762
|
-
# "description"=>[nil, "A campaign for the summer sale, targeting our loyal customers."],
|
763
|
-
# "trigger"=>[nil, "SummerStart"]
|
764
|
-
# }
|
765
|
-
|
766
|
-
campaign.emails.each do |email|
|
767
|
-
email.new_record? # true
|
768
|
-
email.changes
|
769
|
-
# {
|
770
|
-
# "subject"=>[nil, "Summer Sale Special Offer ..."],
|
771
|
-
# "body"=>[nil, "Dear Customer, check out our exclusive summer sale offers! ..."],
|
772
|
-
# "wait"=>[nil, ...]
|
773
|
-
# }
|
774
|
-
|
775
|
-
email.attachments.each do |attachment|
|
776
|
-
attachment.new_record? # true
|
777
|
-
attachment.changes
|
778
|
-
# {
|
779
|
-
# "file_name"=>[nil, "summer_sale_..._attachment_....pdf"],
|
780
|
-
# "content_type"=>[nil, "application/pdf"],
|
781
|
-
# "file_size"=>[nil, ...],
|
782
|
-
# "file_data"=>[nil, "..."]
|
783
|
-
# }
|
784
|
-
end
|
785
|
-
end
|
359
|
+
uid = URI::UID.parse(uri)
|
360
|
+
#=> #<URI::UID payload=CwSAgtQAYQHUAGMDAw, fingerprint=CwSAkccFf6RIYXNoAw>
|
786
361
|
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
restored.new_record? # true
|
791
|
-
restored.changes
|
792
|
-
# {
|
793
|
-
# "name"=>[nil, "Summer Sale Campaign"],
|
794
|
-
# "description"=>[nil, "A campaign for the summer sale, targeting our loyal customers."],
|
795
|
-
# "trigger"=>[nil, "SummerStart"]
|
796
|
-
# }
|
797
|
-
|
798
|
-
restored.emails.each do |email|
|
799
|
-
email.new_record? # true
|
800
|
-
email.changes
|
801
|
-
# {
|
802
|
-
# "subject"=>[nil, "Summer Sale Special Offer ..."],
|
803
|
-
# "body"=>[nil, "Dear Customer, check out our exclusive summer sale offers! ..."],
|
804
|
-
# "wait"=>[nil, ...]
|
805
|
-
# }
|
806
|
-
|
807
|
-
email.attachments.each do |attachment|
|
808
|
-
attachment.new_record? # true
|
809
|
-
attachment.changes
|
810
|
-
# {
|
811
|
-
# "file_name"=>[nil, "summer_sale_..._attachment_....pdf"],
|
812
|
-
# "content_type"=>[nil, "application/pdf"],
|
813
|
-
# "file_size"=>[nil, ...],
|
814
|
-
# "file_data"=>[nil, "..."]
|
815
|
-
# }
|
816
|
-
end
|
817
|
-
end
|
818
|
-
```
|
819
|
-
</details>
|
820
|
-
|
821
|
-
<details>
|
822
|
-
<summary><b>How to Include Unsaved Changes for Persisted Records</b>... ▾</summary>
|
823
|
-
<p></p>
|
824
|
-
|
825
|
-
```ruby
|
826
|
-
# NOTE: The campaign model instance was setup earlier in the "Model Instances" section above
|
827
|
-
# persist the model and its associations
|
828
|
-
campaign.save!
|
829
|
-
|
830
|
-
# make some unsaved changes to the records
|
831
|
-
campaign.name = "Changed Name #{SecureRandom.hex}"
|
832
|
-
campaign.emails.each do |email|
|
833
|
-
email.subject = "Changed Subject #{SecureRandom.hex}"
|
834
|
-
email.attachments.each do |attachment|
|
835
|
-
attachment.file_name = "changed_file_name#{SecureRandom.hex}.pdf"
|
836
|
-
end
|
837
|
-
end
|
362
|
+
uid.decode
|
363
|
+
#=> {:a=>1, :c=>3}
|
364
|
+
```
|
838
365
|
|
839
|
-
|
840
|
-
|
841
|
-
# {"name"=>["Summer Sale Campaign", "Changed Name ..."]}
|
366
|
+
> [!NOTE]
|
367
|
+
> Options can be passed in structured or flat format.
|
842
368
|
|
843
|
-
|
844
|
-
email.persisted? # true
|
845
|
-
email.changes
|
846
|
-
# {"subject"=>["Summer Sale Special Offer 1", "Changed Subject ..."]}
|
369
|
+
It's also possible to register frequently used options.
|
847
370
|
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
end
|
853
|
-
end
|
371
|
+
```yaml
|
372
|
+
# app/config/changed.yml
|
373
|
+
prepack:
|
374
|
+
include_blank: false
|
854
375
|
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
include_descendants: true,
|
376
|
+
database:
|
377
|
+
include_changes: true
|
378
|
+
include_descendants: true
|
859
379
|
descendant_depth: 2
|
860
|
-
|
380
|
+
```
|
861
381
|
|
862
|
-
|
863
|
-
|
382
|
+
```ruby
|
383
|
+
UniversalID::Settings.register :changed, File.expand_path("app/config/changed.yml", __dir__)
|
384
|
+
uid = URI::UID.build(record, UniversalID::Settings[:changed])
|
385
|
+
```
|
864
386
|
|
865
|
-
|
866
|
-
restored.changes
|
867
|
-
# {"name"=>["Summer Sale Campaign", "Changed Name ..."]}
|
387
|
+
## Advanced Usage
|
868
388
|
|
869
|
-
|
870
|
-
email.persisted? # true
|
871
|
-
email.changes
|
872
|
-
# {"subject"=>["Summer Sale Special Offer 1", "Changed Subject ..."]}
|
389
|
+
### Fingerprinting
|
873
390
|
|
874
|
-
|
875
|
-
attachment.persisted? # true
|
876
|
-
attachment.changes
|
877
|
-
# {"file_name"=>["summer_sale_..._attachment_....pdf", "changed_file_name....pdf"]}
|
878
|
-
end
|
879
|
-
end
|
880
|
-
```
|
881
|
-
</details>
|
882
|
-
|
883
|
-
<details>
|
884
|
-
<summary><b>How to Copy Persisted Records</b>... ▾</summary>
|
885
|
-
<p></p>
|
886
|
-
|
887
|
-
```ruby
|
888
|
-
# NOTE: The campaign model instance was setup earlier in the "Model Instances" section above
|
889
|
-
# persist the model and its associations
|
890
|
-
campaign.save!
|
891
|
-
|
892
|
-
options = {
|
893
|
-
include_keys: false,
|
894
|
-
include_timestamps: false,
|
895
|
-
include_unsaved_changes: true,
|
896
|
-
include_descendants: true,
|
897
|
-
descendant_depth: 2
|
898
|
-
}
|
391
|
+
Each UID is fingerprinted as part of the serialization process.
|
899
392
|
|
900
|
-
|
901
|
-
copy = URI::UID.parse(encoded).decode
|
393
|
+
Fingerprints are comprised of the following components:
|
902
394
|
|
903
|
-
|
904
|
-
|
905
|
-
copy.save!
|
395
|
+
1. `Class (Class)` - The encoded object's class
|
396
|
+
2. `Timestamp (Time)` - The `mtime` (UTC) of the file that defined the object's class
|
906
397
|
|
907
|
-
|
398
|
+
Fingerprints provide a simple mechanism to help manage data format versions... **minimizing the need for custom versioning solutions**.
|
399
|
+
Whenever the class definition changes, the `mtime` updates, resulting in a different fingerprint.
|
400
|
+
This is especially useful in scenarios where the data format evolves over time, such as in long-lived applications.
|
908
401
|
|
909
|
-
|
910
|
-
|
911
|
-
campaign_email_ids = campaign.emails.map(&:id)
|
912
|
-
(copy_email_ids && campaign_email_ids).any? # false
|
402
|
+
```ruby
|
403
|
+
uid = URI::UID.build(campaign)
|
913
404
|
|
914
|
-
|
915
|
-
|
916
|
-
(copy_attachment_ids & campaign_attachment_ids).any? # false
|
917
|
-
end
|
918
|
-
```
|
919
|
-
</details>
|
405
|
+
uid.fingerprint
|
406
|
+
#=> "CwuAkscJf6hDYW1wYWlnbtf_ReuZnGWeG5MD"
|
920
407
|
|
921
|
-
|
408
|
+
uid.fingerprint(decode: true)
|
409
|
+
#=> [Campaign(id: integer, ...), <Time>]
|
410
|
+
```
|
922
411
|
|
923
|
-
|
412
|
+
> [!NOTE]
|
413
|
+
> The timestamp or `mtime` is determined the moment a UID is created.
|
924
414
|
|
925
|
-
>
|
415
|
+
> [!TIP]
|
416
|
+
> Fingerprints can help you maintain consistency and reliability when working with serialized data over time.
|
417
|
+
> While fingerpint creation is automatic and implicit, usage is optional... ready whenever you need it.
|
926
418
|
|
927
|
-
|
928
|
-
<summary><b>How to work with ActiveRecord::Relations</b>... ▾</summary>
|
929
|
-
<p></p>
|
419
|
+
### Copy ActiveRecord Models
|
930
420
|
|
931
|
-
|
932
|
-
# Assuming we have multiple campaigns already stored in the database
|
933
|
-
relation = Campaign.joins(:emails).where("emails.subject LIKE ?", "Flash Sale%")
|
421
|
+
Make a copy of an ActiveRecord model _(with loaded associations)_.
|
934
422
|
|
935
|
-
|
936
|
-
|
937
|
-
|
423
|
+
```ruby
|
424
|
+
campaign = Campaign.first
|
425
|
+
|
426
|
+
# ensure desired associations are loaded so they can be included in an UID
|
427
|
+
campaign.emails.load
|
428
|
+
campaign.emails.each { |e| e.attachments.load }
|
429
|
+
|
430
|
+
# introspection
|
431
|
+
campaign.id #=> 1
|
432
|
+
campaign.emails.map(&:id) #=> [1, 2]
|
433
|
+
campaign.emails.map(&:attachments).flatten.map(&:id)
|
434
|
+
#=> [1, 2, 3, 4]
|
435
|
+
|
436
|
+
# setup options for copying
|
437
|
+
options = {
|
438
|
+
include_blank: false,
|
439
|
+
include_keys: false,
|
440
|
+
include_timestamps: false,
|
441
|
+
include_descendants: true,
|
442
|
+
descendant_depth: 2
|
443
|
+
}
|
444
|
+
|
445
|
+
uri = URI::UID.build(campaign, options).to_s
|
446
|
+
#=> "uid://universalid/G7kAIBylMxZa7MouY3gUqHKkIx3hk4s8NT5xWwQsDc7lKUkGWM4DHsCxQZK..."
|
938
447
|
|
939
|
-
|
940
|
-
|
448
|
+
uid = URI::UID.parse(uri)
|
449
|
+
#=> #<URI::UID payload=G7kAIBylMxZa7MouY3gUqHKkIx3hk4s8NT5xW..., fingerprint=CwuAkscJf6hDYW1wYWlnbtf_ReuZnGWeG5MD>
|
941
450
|
|
942
|
-
|
943
|
-
|
944
|
-
decoded == relation # true
|
945
|
-
decoded.size == relation.size # true
|
946
|
-
decoded.to_a == relation.to_a # true
|
947
|
-
```
|
948
|
-
</details>
|
451
|
+
copy = uid.decode
|
452
|
+
#=> #<Campaign:0x00000001135c7448 id: nil, name: "My Campaign", ...>
|
949
453
|
|
950
|
-
|
454
|
+
copy == campaign
|
455
|
+
#=> false
|
951
456
|
|
952
|
-
|
953
|
-
|
954
|
-
|
457
|
+
# introspection
|
458
|
+
copy.new_record? #=> true
|
459
|
+
copy.id #=> nil
|
460
|
+
copy.emails.map(&:id) #=> [nil, nil]
|
461
|
+
copy.emails.map(&:attachments).flatten.map(&:id)
|
462
|
+
#=> [nil, nil, nil, nil]
|
955
463
|
|
956
|
-
|
957
|
-
|
958
|
-
|
464
|
+
# create the copy (new records) in the database
|
465
|
+
copy.save #=> true
|
466
|
+
```
|
959
467
|
|
960
|
-
|
961
|
-
|
962
|
-
name: "Wireless Bluetooth Headphones",
|
963
|
-
price: 79.99,
|
964
|
-
category: "Electronics"
|
965
|
-
}
|
468
|
+
> [!TIP]
|
469
|
+
> If you don't need a URL-Safe UID, you can use `UniversalID::Packer` to speed things up a bit.
|
966
470
|
|
967
|
-
|
968
|
-
|
471
|
+
```ruby
|
472
|
+
packed = UniversalID::Packer.pack(campaign, options)
|
473
|
+
copy = UniversalID::Packer.unpack(packed)
|
474
|
+
copy.save
|
475
|
+
```
|
969
476
|
|
970
|
-
|
971
|
-
#=> "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJZ0d4WjJsa09pOHZkVzVwZG1WeWMyRnNMV2xrTDFWU1NUbzZWVWxFT2pwSGJHOWlZV3hKUkZKbFkyOXlaQzlITUhO..."
|
477
|
+
### ActiveRecord::Relations
|
972
478
|
|
973
|
-
|
974
|
-
|
975
|
-
# name: "Wireless Bluetooth Headphones",
|
976
|
-
# price: 79.99,
|
977
|
-
# category: "Electronics"
|
978
|
-
# }
|
479
|
+
Universal ID also supports ActiveRecord relations/scopes.
|
480
|
+
You can easily serialize complex queries into a portable and sharable format.
|
979
481
|
|
482
|
+
```ruby
|
483
|
+
relation = Campaign.joins(:emails).where("emails.subject LIKE ?", "Flash Sale%")
|
980
484
|
|
981
|
-
|
982
|
-
|
983
|
-
#=> nil
|
984
|
-
```
|
985
|
-
</details>
|
485
|
+
uri = URI::UID.build(relation).to_s
|
486
|
+
#=> "uid://universalid/G90EQCwLeEP1oQtHFksrdN5YS4ju5TryFZwBJgh2toqS3SKEVSl1FoNtZjI..."
|
986
487
|
|
987
|
-
|
488
|
+
uid = URI::UID.parse(encoded)
|
489
|
+
#=> #<URI::UID payload=G90EQCwLeEP1oQtHFksrdN5YS4ju5TryFZwBJ..., fingerprint=CxKAkscXf7ZBY3RpdmVSZWNvcmQ6OlJlbGF0a...>
|
988
490
|
|
989
|
-
|
990
|
-
UIDs automatically include a "fingerprint" for each serialized object based on the target object's class and
|
991
|
-
its modification time _(mtime)_.
|
491
|
+
decoded = uid.decode
|
992
492
|
|
993
|
-
|
493
|
+
# introspection
|
494
|
+
decoded == relation #=> true
|
495
|
+
decoded.is_a? ActiveRecord::Relation #=> true
|
496
|
+
decoded.loaded? #=> false
|
994
497
|
|
995
|
-
|
996
|
-
|
498
|
+
# run the query
|
499
|
+
campaigns = decoded.load
|
500
|
+
```
|
997
501
|
|
998
|
-
>
|
502
|
+
> [!NOTE]
|
503
|
+
> Universal ID clears cached data within the relation before encoding. This minimizes payload size while preserving the integrity of the underlying query.
|
999
504
|
|
1000
|
-
|
1001
|
-
Whenever the class definition changes, the mtime updates, resulting in a different fingerprint.
|
1002
|
-
This is especially useful in scenarios where the data format evolves over time, such as in long-lived applications.
|
505
|
+
### SignedGlobalID
|
1003
506
|
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
507
|
+
Features like `signing` _(to prevent tampering)_, `purpose`, and `expiration` are provided by SignedGlobalIDs.
|
508
|
+
These features _(and more)_ will eventually be added to Universal ID, but until then...
|
509
|
+
simply convert your UID to a SignedGlobalID to add these features to any Universal ID.
|
1007
510
|
|
1008
|
-
|
511
|
+
```ruby
|
512
|
+
data = OpenStruct.new(name: "Demo", value: "Example")
|
1009
513
|
|
1010
|
-
|
1011
|
-
|
1012
|
-
campaign.save!
|
514
|
+
sgid = URI::UID.build(data).to_sgid_param(for: "purpose", expires_in: 1.hour)
|
515
|
+
#=> "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJZ0plQTJkcFpEb3ZMM1Z1YVhabGNuTmhiQzFwWkM5V..."
|
1013
516
|
|
1014
|
-
|
1015
|
-
|
1016
|
-
# | encoding options (whatever was passed to URI::UID.build or {})
|
1017
|
-
# | |
|
1018
|
-
uid = URI::UID.build(campaign) do |record, options|
|
1019
|
-
# NOTE: this feature is compatible with other tools like kiba, amoeba, etc.
|
1020
|
-
# that can be leveraged to generate the data to be encoded
|
517
|
+
uid = URI::UID.from_sgid(sgid, for: "purpose")
|
518
|
+
#=> #<URI::UID payload=Cw-Axxx-gtYAbmFtZaREZW1vxwUAdmFsdWWnR..., fingerprint=ixqAkscof9kmVW5pdmVyc2FsSUQ6OkV4dGVuc...>
|
1021
519
|
|
1022
|
-
|
520
|
+
decoded = uid.decode
|
521
|
+
#=> #<OpenStruct name="Demo", value="Example">
|
1023
522
|
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
2. Decode the UID using a custom handler _(optional Ruby block)_. This allows you to take control of the decoding process.
|
1029
|
-
|
1030
|
-
```ruby
|
1031
|
-
# fingerprint components
|
1032
|
-
# ____|______
|
1033
|
-
# the decoded payload from above | |
|
1034
|
-
# | | |
|
1035
|
-
decoded = URI::UID.parse(uid.to_s).decode do |data, klass, timestamp|
|
1036
|
-
# NOTE: this feature is compatible with other tools like kiba, amoeba, etc.
|
1037
|
-
# that can be leveraged to translate the decoded data into the correct format
|
1038
|
-
|
1039
|
-
record = klass.find_by(id: data[:id])
|
1040
|
-
record.instance_variable_set(:@demo, data[:demo])
|
1041
|
-
|
1042
|
-
case Time.parse(timestamp)
|
1043
|
-
when 3.months.ago..Time.now
|
1044
|
-
# current data format
|
1045
|
-
# return the record as-is
|
1046
|
-
record
|
1047
|
-
when 1.year.ago..3.months.ago
|
1048
|
-
# outdated data format
|
1049
|
-
# apply an ETL process to bring the outdated data current
|
1050
|
-
# then return the modified record
|
1051
|
-
record
|
1052
|
-
end
|
1053
|
-
end
|
1054
|
-
```
|
1055
|
-
</details>
|
1056
|
-
|
1057
|
-
Fingerprinting allows for seamless handling of different data versions and formats,
|
1058
|
-
so you can maintain consistency and reliability in applications dealing with serialized data over time.
|
1059
|
-
|
1060
|
-
> :bulb: **Optional Usage**: While fingerpint creation is automatic and implicit, using it is optional... ready whenever you want more control.
|
1061
|
-
|
1062
|
-
## Performance and Benchmarks
|
1063
|
-
|
1064
|
-
<details>
|
1065
|
-
<summary><b>View Benchmarks</b>... ▾</summary>
|
1066
|
-
<p></p>
|
1067
|
-
|
1068
|
-
Benchmarks can be performed by cloning the project and running `bin/bench`.
|
1069
|
-
The run below was performed on the following hardware.
|
1070
|
-
|
1071
|
-
```
|
1072
|
-
Model Name: MacBook Air
|
1073
|
-
Model Identifier: MacBookAir10,1
|
1074
|
-
Chip: Apple M1
|
1075
|
-
Total Number of Cores: 8 (4 performance and 4 efficiency)
|
1076
|
-
Memory: 16 GB
|
1077
|
-
```
|
1078
|
-
|
1079
|
-
```
|
1080
|
-
Benchmarking with the following ActiveRecord/Hash data...
|
1081
|
-
==================================================================================================
|
1082
|
-
{
|
1083
|
-
"id" => 1,
|
1084
|
-
"name" => "Production",
|
1085
|
-
"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.",
|
1086
|
-
"trigger" => "Political Organization enhance web-enabled architectures",
|
1087
|
-
"created_at" => "2023-11-11T01:28:46.657Z",
|
1088
|
-
"updated_at" => "2023-11-11T01:28:46.657Z",
|
1089
|
-
"emails" => [
|
1090
|
-
[0] {
|
1091
|
-
"id" => 1,
|
1092
|
-
"campaign_id" => 1,
|
1093
|
-
"subject" => "drive synergistic web-readiness",
|
1094
|
-
"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?)",
|
1095
|
-
"wait" => nil,
|
1096
|
-
"created_at" => "2023-11-11T01:28:46.661Z",
|
1097
|
-
"updated_at" => "2023-11-11T01:28:46.675Z",
|
1098
|
-
"attachments" => [
|
1099
|
-
[0] {
|
1100
|
-
"id" => 1,
|
1101
|
-
"email_id" => 1,
|
1102
|
-
"file_name" => "Schneider and Sons",
|
1103
|
-
"content_type" => "Enterprise-wide 4th generation complexity",
|
1104
|
-
"file_size" => nil,
|
1105
|
-
"file_data" => nil,
|
1106
|
-
"created_at" => "2023-11-11T01:28:46.664Z",
|
1107
|
-
"updated_at" => "2023-11-11T01:28:46.670Z"
|
1108
|
-
},
|
1109
|
-
[1] {
|
1110
|
-
"id" => 2,
|
1111
|
-
"email_id" => 1,
|
1112
|
-
"file_name" => "Devolved solution-oriented circuit",
|
1113
|
-
"content_type" => "revolutionize magnetic bandwidth Intelligent Paper Gloves",
|
1114
|
-
"file_size" => nil,
|
1115
|
-
"file_data" => nil,
|
1116
|
-
"created_at" => "2023-11-11T01:28:46.664Z",
|
1117
|
-
"updated_at" => "2023-11-11T01:28:46.670Z"
|
1118
|
-
}
|
1119
|
-
]
|
1120
|
-
},
|
1121
|
-
[1] {
|
1122
|
-
"id" => 2,
|
1123
|
-
"campaign_id" => 1,
|
1124
|
-
"subject" => "Marketing",
|
1125
|
-
"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.",
|
1126
|
-
"wait" => nil,
|
1127
|
-
"created_at" => "2023-11-11T01:28:46.671Z",
|
1128
|
-
"updated_at" => "2023-11-11T01:28:46.675Z",
|
1129
|
-
"attachments" => [
|
1130
|
-
[0] {
|
1131
|
-
"id" => 3,
|
1132
|
-
"email_id" => 2,
|
1133
|
-
"file_name" => "Weber-Schulist benchmark open-source applications",
|
1134
|
-
"content_type" => "Enormous Linen Shoes synthesize customized e-services",
|
1135
|
-
"file_size" => nil,
|
1136
|
-
"file_data" => nil,
|
1137
|
-
"created_at" => "2023-11-11T01:28:46.672Z",
|
1138
|
-
"updated_at" => "2023-11-11T01:28:46.672Z"
|
1139
|
-
},
|
1140
|
-
[1] {
|
1141
|
-
"id" => 4,
|
1142
|
-
"email_id" => 2,
|
1143
|
-
"file_name" => "thought leadership",
|
1144
|
-
"content_type" => "Business Development Enhanced logistical collaboration",
|
1145
|
-
"file_size" => nil,
|
1146
|
-
"file_data" => nil,
|
1147
|
-
"created_at" => "2023-11-11T01:28:46.672Z",
|
1148
|
-
"updated_at" => "2023-11-11T01:28:46.672Z"
|
1149
|
-
}
|
1150
|
-
]
|
1151
|
-
},
|
1152
|
-
[2] {
|
1153
|
-
"id" => 3,
|
1154
|
-
"campaign_id" => 1,
|
1155
|
-
"subject" => "Mediocre Aluminum Car",
|
1156
|
-
"body" => "Don’t even trip dawg. Stay away from my special lady friend, man.",
|
1157
|
-
"wait" => nil,
|
1158
|
-
"created_at" => "2023-11-11T01:28:46.672Z",
|
1159
|
-
"updated_at" => "2023-11-11T01:28:46.675Z",
|
1160
|
-
"attachments" => [
|
1161
|
-
[0] {
|
1162
|
-
"id" => 5,
|
1163
|
-
"email_id" => 3,
|
1164
|
-
"file_name" => "Import and Export",
|
1165
|
-
"content_type" => "Heavy Duty Paper Bench Project Management",
|
1166
|
-
"file_size" => nil,
|
1167
|
-
"file_data" => nil,
|
1168
|
-
"created_at" => "2023-11-11T01:28:46.673Z",
|
1169
|
-
"updated_at" => "2023-11-11T01:28:46.674Z"
|
1170
|
-
},
|
1171
|
-
[1] {
|
1172
|
-
"id" => 6,
|
1173
|
-
"email_id" => 3,
|
1174
|
-
"file_name" => "synthesize ubiquitous architectures Corporate Communications",
|
1175
|
-
"content_type" => "Durable Rubber Watch",
|
1176
|
-
"file_size" => nil,
|
1177
|
-
"file_data" => nil,
|
1178
|
-
"created_at" => "2023-11-11T01:28:46.674Z",
|
1179
|
-
"updated_at" => "2023-11-11T01:28:46.674Z"
|
1180
|
-
}
|
1181
|
-
]
|
1182
|
-
}
|
1183
|
-
]
|
1184
|
-
}
|
1185
|
-
==================================================================================================
|
1186
|
-
Benchmarking 5000 iterations
|
1187
|
-
==================================================================================================
|
1188
|
-
user system total real
|
1189
|
-
URI::UID.build Hash 14.770667 0.102535 14.873202 ( 14.898856)
|
1190
|
-
Average 0.002954 0.000021 0.002975 ( 0.002980)
|
1191
|
-
..................................................................................................
|
1192
|
-
user system total real
|
1193
|
-
URI::UID.build Hash, include_blank: false 13.821420 0.066910 13.888330 ( 13.892066)
|
1194
|
-
Average 0.002764 0.000013 0.002778 ( 0.002778)
|
1195
|
-
..................................................................................................
|
1196
|
-
user system total real
|
1197
|
-
URI::UID.parse HASH/UID 0.075566 0.000411 0.075977 ( 0.076035)
|
1198
|
-
Average 0.000015 0.000000 0.000015 ( 0.000015)
|
1199
|
-
..................................................................................................
|
1200
|
-
user system total real
|
1201
|
-
URI::UID.decode HASH/UID 0.111007 0.003572 0.114579 ( 0.114587)
|
1202
|
-
Average 0.000022 0.000001 0.000023 ( 0.000023)
|
1203
|
-
..................................................................................................
|
1204
|
-
user system total real
|
1205
|
-
URI::UID.build ActiveRecord 0.984594 0.010059 0.994653 ( 0.994662)
|
1206
|
-
Average 0.000197 0.000002 0.000199 ( 0.000199)
|
1207
|
-
..................................................................................................
|
1208
|
-
user system total real
|
1209
|
-
URI::UID.build ActiveRecord, exclude_blank 0.953653 0.006692 0.960345 ( 0.960765)
|
1210
|
-
Average 0.000191 0.000001 0.000192 ( 0.000192)
|
1211
|
-
..................................................................................................
|
1212
|
-
user system total real
|
1213
|
-
URI::UID.build ActiveRecord, include_descendants 44.958468 0.170125 45.128593 ( 45.176116)
|
1214
|
-
Average 0.008992 0.000034 0.009026 ( 0.009035)
|
1215
|
-
..................................................................................................
|
1216
|
-
user system total real
|
1217
|
-
URI::UID.parse ActiveRecord/UID 0.119030 0.000319 0.119349 ( 0.119525)
|
1218
|
-
Average 0.000024 0.000000 0.000024 ( 0.000024)
|
1219
|
-
..................................................................................................
|
1220
|
-
user system total real
|
1221
|
-
URI::UID.decode HASH/UID 5.198092 0.024652 5.222744 ( 5.282794)
|
1222
|
-
Average 0.001040 0.000005 0.001045 ( 0.001057)
|
1223
|
-
..................................................................................................
|
1224
|
-
user system total real
|
1225
|
-
UID > GID > UID.decode include_descendants 55.612061 0.398193 56.010254 ( 57.372350)
|
1226
|
-
Average 0.011122 0.000080 0.011202 ( 0.011474)
|
1227
|
-
..................................................................................................
|
1228
|
-
user system total real
|
1229
|
-
UID > SGID > UID.decode include_descendants 55.406590 0.260552 55.667142 ( 56.432082)
|
1230
|
-
Average 0.011081 0.000052 0.011133 ( 0.011286)
|
1231
|
-
..................................................................................................
|
1232
|
-
```
|
1233
|
-
</details>
|
523
|
+
# a mismatched purpose returns nil... as expected
|
524
|
+
URI::UID.from_sgid(sgid, for: "mismatch")
|
525
|
+
#=> nil
|
526
|
+
```
|
1234
527
|
|
1235
528
|
## Sponsors
|
1236
529
|
|