universalid 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +387 -1094
  3. data/Rakefile +16 -2
  4. data/config/default.yml +1 -1
  5. data/config/example.yml +2 -2
  6. data/lib/universalid/encoder.rb +21 -0
  7. data/lib/universalid/extensions/active_record/base_message_pack_type.rb +15 -0
  8. data/lib/universalid/extensions/active_record/base_packer.rb +170 -0
  9. data/lib/universalid/extensions/active_record/base_unpacker.rb +61 -0
  10. data/lib/universalid/extensions/active_record/relation_message_pack_type.rb +20 -0
  11. data/lib/universalid/extensions/active_support/time_with_zone_message_pack_type.rb +18 -0
  12. data/lib/universalid/extensions/global_id/global_id_model.rb +28 -0
  13. data/lib/universalid/extensions/global_id/global_id_uid_extension.rb +43 -0
  14. data/lib/universalid/extensions/global_id/message_pack_type.rb +16 -0
  15. data/lib/universalid/extensions/signed_global_id/message_pack_type.rb +12 -0
  16. data/lib/{universal_id → universalid}/message_pack_types/ruby/composites/open_struct.rb +2 -0
  17. data/lib/universalid/message_pack_types/ruby/scalars/bigdecimal.rb +10 -0
  18. data/lib/{universal_id → universalid}/message_pack_types/ruby/scalars/date.rb +2 -0
  19. data/lib/{universal_id → universalid}/message_pack_types/ruby/scalars/date_time.rb +2 -0
  20. data/lib/{universal_id → universalid}/message_pack_types.rb +3 -5
  21. data/lib/universalid/packer.rb +19 -0
  22. data/lib/{universal_id → universalid}/prepack_database_options.rb +4 -4
  23. data/lib/{universal_id → universalid}/prepack_options.rb +2 -12
  24. data/lib/{universal_id → universalid}/prepacker.rb +3 -3
  25. data/lib/{universal_id → universalid}/refinements/open_struct_refinement.rb +2 -0
  26. data/lib/universalid/refinements.rb +3 -0
  27. data/lib/{universal_id → universalid}/settings.rb +3 -2
  28. data/lib/{universal_id → universalid}/version.rb +1 -1
  29. data/lib/universalid.rb +28 -1
  30. data/lib/uri/uid.rb +17 -7
  31. metadata +128 -50
  32. data/lib/universal_id/contrib/active_record/base_message_pack_type.rb +0 -11
  33. data/lib/universal_id/contrib/active_record/base_packer.rb +0 -130
  34. data/lib/universal_id/contrib/active_record/base_unpacker.rb +0 -50
  35. data/lib/universal_id/contrib/active_record/relation_message_pack_type.rb +0 -16
  36. data/lib/universal_id/contrib/active_record.rb +0 -4
  37. data/lib/universal_id/contrib/active_support/time_with_zone_message_pack_type.rb +0 -14
  38. data/lib/universal_id/contrib/active_support.rb +0 -3
  39. data/lib/universal_id/contrib/global_id/global_id_model.rb +0 -24
  40. data/lib/universal_id/contrib/global_id/global_id_uid_extension.rb +0 -36
  41. data/lib/universal_id/contrib/global_id/message_pack_type.rb +0 -15
  42. data/lib/universal_id/contrib/global_id.rb +0 -3
  43. data/lib/universal_id/contrib/rails.rb +0 -6
  44. data/lib/universal_id/contrib/signed_global_id/message_pack_type.rb +0 -8
  45. data/lib/universal_id/contrib/signed_global_id.rb +0 -3
  46. data/lib/universal_id/encoder.rb +0 -27
  47. data/lib/universal_id/refinements.rb +0 -8
  48. data/lib/universal_id.rb +0 -29
  49. /data/lib/{universal_id → universalid}/message_pack_factory.rb +0 -0
  50. /data/lib/{universal_id → universalid}/message_pack_types/ruby/composites/module.rb +0 -0
  51. /data/lib/{universal_id → universalid}/message_pack_types/ruby/composites/set.rb +0 -0
  52. /data/lib/{universal_id → universalid}/message_pack_types/ruby/composites/struct.rb +0 -0
  53. /data/lib/{universal_id → universalid}/message_pack_types/ruby/scalars/complex.rb +0 -0
  54. /data/lib/{universal_id → universalid}/message_pack_types/ruby/scalars/range.rb +0 -0
  55. /data/lib/{universal_id → universalid}/message_pack_types/ruby/scalars/rational.rb +0 -0
  56. /data/lib/{universal_id → universalid}/message_pack_types/ruby/scalars/regexp.rb +0 -0
  57. /data/lib/{universal_id → universalid}/message_pack_types/uri/uid/type.rb +0 -0
  58. /data/lib/{universal_id → universalid}/refinements/array_refinement.rb +0 -0
  59. /data/lib/{universal_id → universalid}/refinements/hash_refinement.rb +0 -0
  60. /data/lib/{universal_id → universalid}/refinements/set_refinement.rb +0 -0
data/README.md CHANGED
@@ -1,200 +1,77 @@
1
- <p align="center">
2
- <h1 align="center">Universal ID</h1>
3
- <p align="center">
4
- <a href="http://blog.codinghorror.com/the-best-code-is-no-code-at-all/">
5
- <img alt="Lines of Code" src="https://img.shields.io/badge/loc-750-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
- <br>
29
- <a href="https://ruby.social/@hopsoft">
30
- <img alt="Ruby.Social Follow" src="https://img.shields.io/mastodon/follow/000008274?domain=https%3A%2F%2Fruby.social&label=%40hopsoft&style=social">
31
- </a>
32
- <a href="https://twitter.com/hopsoft">
33
- <img alt="Twitter Follow" src="https://img.shields.io/twitter/url?label=%40hopsoft&style=social&url=https%3A%2F%2Ftwitter.com%2Fhopsoft">
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
- **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.
36
+ ## Fast, recursive, and 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
- 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>
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
- - [Scalars](#scalars)
52
- - [Composites](#composites)
52
+ - [Primitive Types](#primitive-types)
53
+ - [Composite Types](#composite-types)
54
+ - [Extension Types](#extension-types)
53
55
  - [Custom Types](#custom-types)
54
- - [Contributed Types](#contributed-types)
55
- - [Settings and Prepack Options](#settings-and-prepack-options)
56
- - [Advanced ActiveRecord](#advanced-activerecord)
57
- - [ActiveRecord::Relation Support](#activerecordrelation-support)
58
- - [SignedGlobalID](#signedglobalid)
59
- - [Fingerprinting (Implicit Versioning)](#fingerprinting-implicit-versioning)
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
- ### Scalars
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 for individual primitives if desired, but scalar support is really the foundation for more serious use cases.
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://universal-id/iwKA1gBkZW1vAw"
93
+ #=> "uid://universalid/iwKA1gBkZW1vAw#CwWAkccHf6ZTeW1ib2wD"
218
94
 
219
95
  uid = URI::UID.parse(uri)
220
- #=> #<URI::UID uid://universal-id/iwKA1gBkZW1vAw>
96
+ #=> #<URI::UID payload=iwKA1gBkZW1vAw, fingerprint=CwWAkccHf6ZTeW1ib2wD>
221
97
 
222
98
  uid.decode
223
99
  #=> :demo
224
100
  ```
225
101
 
226
- ### Composites
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
- uri = URI::UID.build(hash).to_s
256
- #=> "uid://universal-id/CxKAhNQAYQHUAGIC1ABjA8cFAGFycmF5lAEC..."
104
+ Composite _(or compound, complex, etc.)_ datatype support is where things start to get interesting.
105
+ Universal ID supports the following native Ruby composite datatypes:
257
106
 
258
- uid = URI::UID.parse(uri)
259
- #=> #<URI::UID uid://universal-id/CxKAhNQAYQHUAGIC1ABjA8cFAGFycmF5lAECA5TUAGHUAGLUAGORwwM>
107
+ - `Array`
108
+ - `Hash`
109
+ - `OpenStruct`
110
+ - `Set`
111
+ - `Struct`
260
112
 
261
- uid.decode
262
- #=> {:a=>1, :b=>2, :c=>3, :array=>[1, 2, 3, [:a, :b, :c, [true]]]}
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
- uri = URI::UID.build(set).to_s
297
- #=> "uid://universal-id/iwiA2AuUAQIDlNQAYdQAYtQAY5HDAw"
116
+ uri = URI::UID.build(array).to_s
117
+ #=> "uid://universalid/iweAlAECA5TUAGHUAGLUAGORwwM#iwSAkccGf6VBcnJheQM"
298
118
 
299
- uid = URI::UID.parse(uri)
300
- #=> #<URI::UID uid://universal-id/iwiA2AuUAQIDlNQAYdQAYtQAY5HDAw>
119
+ uid = URI::UID.parse(uri)
120
+ #=> #<URI::UID payload=iweAlAECA5TUAGHUAGLUAGORwwM, fingerprint=iwSAkccGf6VBcnJheQM>
301
121
 
302
- URI::UID.parse(uri).decode
303
- #=> #<Set: {1, 2, 3, [:a, :b, :c, [true]]}>
304
- ```
305
- </details>
122
+ uid.decode
123
+ #=> [1, 2, 3, [:a, :b, :c, [true]]]
306
124
 
307
- <details>
308
- <summary><b><code><></code> Struct</b>... ▾</summary>
309
- <p></p>
125
+ uid.decode == array
126
+ #=> true
127
+ ```
310
128
 
311
- ```ruby
312
- Book = Struct.new(:title, :author, :isbn, :published_year)
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
- uri = URI::UID.build(book).to_s
316
- #=> "uid://universal-id/G2YAoGTomv9N_4RV2oJRxRvZdC1wNJ0H3Ipu45kVcSrAxtg6Wjtogpi6GV1XXQAOAXoNR3BrCg9AQ..."
132
+ uri = URI::UID.build(hash).to_s
133
+ #=> "uid://universalid/CxKAhNQAYQHUAGIC1ABjA8cFAGFycmF5lAECA5TUAGHUAGLUAGORwwM#CwS..."
317
134
 
318
- uid = URI::UID.parse(uri)
319
- #=> #<URI::UID scheme=uid, host=universal-id, payload=G2YAoGTomv9N_4RV2oJRxRvZdC1wNJ0H3Ipu45kVcSrAxtg6Wjtogpi6GV1XXQAOAXoNR3BrCg9AQ...>
135
+ uid = URI::UID.parse(uri)
136
+ #=> #<URI::UID payload=CxKAhNQAYQHUAGIC1ABjA8cFAGFycmF5lAECA..., fingerprint=CwSAkccFf6RIYXNoAw>
320
137
 
321
- uid.decode
322
- #=> #<struct Book title="The Great Gatsby", author="F. Scott Fitzgerald", isbn="9780743273565", published_year=1925>
323
- ```
324
- </details>
138
+ uid.decode
139
+ #=> {:a=>1, :b=>2, :c=>3, :array=>[1, 2, 3, [:a, :b, :c, [true]]]}
325
140
 
326
- ### Custom Types
141
+ uid.decode == hash
142
+ #=> true
143
+ ```
327
144
 
328
- Universal ID is **extensible** so you can register your own datatypes with specialized serialization rules.
329
- It couldn't be simpler. Just convert the required data to a Ruby scalar or composite value.
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
- <details>
332
- <summary><b>How to Register your own Datatype</b>... ▾</summary>
333
- <p></p>
149
+ uri = URI::UID.build(book).to_s
150
+ #=> "uid://universalid/G2YAoGTomv9tT1ilLRgVC9vIpmuBo-k84FZ0G8-siFMBNsbW0dpBE0Tnm96..."
334
151
 
335
- ```ruby
336
- class UserSettings
337
- attr_accessor :user_id, :preferences
152
+ uid = URI::UID.parse(uri)
153
+ #=> #<URI::UID payload=G2YAoGTomv9tT1ilLRgVC9vIpmuBo-k84FZ0G..., fingerprint=CwSAkccFf6RCb29rAw>
338
154
 
339
- def initialize(user_id, preferences = {})
340
- @user_id = user_id
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
- UniversalID::MessagePackFactory.register(
346
- type: UserSettings,
347
- packer: ->(user_preferences, packer) do
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
- settings = UserSettings.new(1,
359
- theme: "dark",
360
- notifications: "email",
361
- language: "en",
362
- layout: "grid",
363
- privacy: "private"
364
- )
162
+ ### Extension Types
365
163
 
366
- uri = URI::UID.build(settings).to_s
367
- #=> "uid://universal-id/G1QAQAT-bfcGW1QOgadJwJF06yL8gDnGgfs1Xdti20TDDvG5STPqzbYcQ6TBqVKhdZ39CdQZUwEGe..."
164
+ The following extension datatypes ship with Universal ID:
368
165
 
369
- uid = URI::UID.parse(uri)
370
- #=> #<URI::UID uid://universal-id/G1QAQAT-bfcGW1QOgadJwJF06yL8gDnGgfs1Xdti20TDDvG5STPqzbYcQ6TBqVKhdZ39CdQZUwEGe..."
166
+ - `ActiveRecord::Base`
167
+ - `ActiveRecord::Relation`
168
+ - `ActiveSupport::TimeWithZone`
169
+ - `GlobalID`
170
+ - `SignedGlobalID`
371
171
 
372
- uid.decode
373
- => #<UserSettings:0x0000000139157dd8 @preferences={:theme=>"dark", :notifications=>"email", :language=>"en", :layout=>"grid", :privacy=>"private"}, @user_id=1>
374
- ```
375
- </details>
172
+ > [!NOTE]
173
+ > Extensions are autoloaded whenever the related datatype is detected.
376
174
 
377
- ### Contributed Types
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
- Universal ID is designed to be highly extensible, allowing for third-party contributions to enhance its capabilities.
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
- - **ActiveRecord::Base**:
384
- Integrates Universal ID with ActiveRecord base models, enabling intelligent serialization of database records.
182
+ - **New Records**:
183
+ Universal ID can serialize models that haven't been saved to the database yet.
385
184
 
386
- - **ActiveRecord::Relation**:
387
- Supports the serialization of ActiveRecord relations, making it possible to encode complex query structures.
185
+ - **Changesets**:
186
+ Universal ID can serialize ActiveRecord models with unsaved changes, ensuring that even transient states are captured.
388
187
 
389
- - **ActiveSupport::TimeWithZone**:
390
- Adds the ability to serialize ActiveSupport's TimeWithZone objects.
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
- - **GlobalID**:
393
- Extends support to include GlobalIDs.
191
+ - **Copying/Cloning**:
192
+ Universal ID supports making copies of records _(including associations)_, making it ideal for duplicating complex datasets.
394
193
 
395
- - **SignedGlobalID**:
396
- Extends support to include SignedGlobalIDs.
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
- **Requiring Contributed Types**
197
+ - **Queries/Relations**:
198
+ Universal ID extends also supports ActiveRecord::Relation, enabling the serialization of complex database queries and scopes.
399
199
 
400
- To utilize the contributed types, you must explicitly require them in your application.
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
- # load contrib types
406
- require "universal_id/contrib/active_record"
407
- require "universal_id/contrib/active_support"
408
- require "universal_id/contrib/global_id"
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
- - **Association Handling**:
430
- Universal ID goes beyond single models. It can serialize associated records, including those with unsaved changes, creating a comprehensive snapshot of complex object states.
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
- - **Cloning Existing Records**:
433
- Need to make a copy of a record, including its associations? Universal ID handles this effortlessly, making it ideal for duplicating complex datasets.
212
+ # make some unsaved changes
213
+ email.subject = "1st Email"
434
214
 
435
- - **Granular Data Control**:
436
- 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.
215
+ # add an unsaved record
216
+ campaign.emails.build(subject: "2nd Email")
437
217
 
438
- - **Efficient Query Serialization**:
439
- 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.
218
+ # introspection
219
+ campaign.emails.size #=> 2
220
+ campaign.emails.loaded? #=> true
221
+ campaign.emails.last.new_record? #=> true
440
222
 
441
- 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.
223
+ options = {
224
+ include_changes: true,
225
+ include_descendants: true,
226
+ descendant_depth: 2
227
+ }
442
228
 
443
- <details>
444
- <summary><b>How to Convert Records to UIDs</b>... ▾</summary>
445
- <p></p>
229
+ uri = URI::UID.build(campaign, options).to_s
230
+ #=> "uid://universalid/GxYBYGT6_Xn_OrelIDRWhQQgvbS5gQxV7EJKe3paIiEFmEEc1gLKw8Pl2-k..."
446
231
 
447
- ```ruby
448
- ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
449
-
450
- ActiveRecord::Schema.define do
451
- create_table :campaigns do |t|
452
- t.column :name, :string
453
- t.timestamps
454
- end
455
- end
456
-
457
- class Campaign < ApplicationRecord
458
- end
459
-
460
- # ---
461
-
462
- campaign = Campaign.create(name: "Marketing Campaign")
463
-
464
- uri = URI::UID.build(campaign).to_s
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
- <details>
671
- <summary><b>Setup the Models</b>... ▾</summary>
672
- <p></p>
251
+ ### Custom Types
673
252
 
674
- ```ruby
675
- class Campaign < ApplicationRecord
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
- class Email < ApplicationRecord
680
- belongs_to :campaign
681
- has_many :attachments, dependent: :destroy
682
- end
256
+ ```ruby
257
+ # create a custom type
258
+ class UserSettings
259
+ attr_accessor :user_id, :preferences
683
260
 
684
- class Attachment < ApplicationRecord
685
- belongs_to :email
261
+ def initialize(user_id, preferences = {})
262
+ @user_id = user_id
263
+ @preferences = preferences
686
264
  end
687
- ```
688
- </details>
689
-
690
-
691
- <details>
692
- <summary><b>Setup the Model Instances</b>... ▾</summary>
693
- <p></p>
694
-
695
- ```ruby
696
- campaign = Campaign.new(
697
- name: "Summer Sale Campaign",
698
- description: "A campaign for the summer sale, targeting our loyal customers.",
699
- trigger: "SummerStart"
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
- # demonstrate that we have new unsaved records
725
-
726
- #campaign
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
- # emails
731
- campaign.emails.each do |email|
732
- email.new_record? # true
733
- email.changed? # true
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
- email.attachments.each do |attachment|
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
- Now let's look at how to leverage Universal ID with ActiveRecord.
353
+ ```ruby
354
+ hash = { a: 1, b: 2, c: 3 }
744
355
 
745
- <details>
746
- <summary><b>How to Include Unsaved Changes for New Records</b>... ▾</summary>
747
- <p></p>
356
+ uri = URI::UID.build(hash, exclude: [:b]).to_s
357
+ #=> "uid://universalid/CwSAgtQAYQHUAGMDAw#CwSAkccFf6RIYXNoAw"
748
358
 
749
- ```ruby
750
- # prepack options
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
- encoded = URI::UID.build(campaign, options).to_s
788
- restored = URI::UID.parse(encoded).decode
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
- campaign.persisted? # true
840
- campaign.changes
841
- # {"name"=>["Summer Sale Campaign", "Changed Name ..."]}
366
+ > [!NOTE]
367
+ > Options can be passed in structured or flat format.
842
368
 
843
- campaign.emails.each do |email|
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
- email.attachments.each do |attachment|
849
- attachment.persisted? # true
850
- attachment.changes
851
- # {"file_name"=>["summer_sale_..._attachment_....pdf", "changed_file_name....pdf"]}
852
- end
853
- end
371
+ ```yaml
372
+ # app/config/changed.yml
373
+ prepack:
374
+ include_blank: false
854
375
 
855
- # prepack options
856
- options = {
857
- include_unsaved_changes: true,
858
- include_descendants: true,
376
+ database:
377
+ include_changes: true
378
+ include_descendants: true
859
379
  descendant_depth: 2
860
- }
380
+ ```
861
381
 
862
- encoded = URI::UID.build(campaign, options).to_s
863
- restored = URI::UID.parse(encoded).decode
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
- restored.persisted? # true
866
- restored.changes
867
- # {"name"=>["Summer Sale Campaign", "Changed Name ..."]}
387
+ ## Advanced Usage
868
388
 
869
- restored.emails.each do |email|
870
- email.persisted? # true
871
- email.changes
872
- # {"subject"=>["Summer Sale Special Offer 1", "Changed Subject ..."]}
389
+ ### Fingerprinting
873
390
 
874
- email.attachments.each do |attachment|
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
- encoded = URI::UID.build(campaign, options).to_s
901
- copy = URI::UID.parse(encoded).decode
393
+ Fingerprints are comprised of the following components:
902
394
 
903
- campaign.persisted? # false
904
- copy.new_record? # true
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
- copy == campaign # false
398
+ Fingerprints providate a simple mechanism to help manage versions of the data format... **without the need for explicit versioning**.
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
- campaign.emails.each do |email|
910
- copy_email_ids = copy.emails.map(&:id)
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
- copy_attachment_ids = copy.emails.map(&:attachments).flatten.map(&:id)
915
- campaign_attachment_ids = campaign.emails.map(&:attachments).flatten.map(&:id)
916
- (copy_attachment_ids & campaign_attachment_ids).any? # false
917
- end
918
- ```
919
- </details>
405
+ uid.fingerprint
406
+ #=> "CwuAkscJf6hDYW1wYWlnbtf_ReuZnGWeG5MD"
920
407
 
921
- ## ActiveRecord::Relation Support
408
+ uid.fingerprint(decode: true)
409
+ #=> [Campaign(id: integer, ...), <Time>]
410
+ ```
922
411
 
923
- 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.
412
+ > [!NOTE]
413
+ > The timestamp or `mtime` is determined the moment a UID is created.
924
414
 
925
- > :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.
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
- <details>
928
- <summary><b>How to work with ActiveRecord::Relations</b>... ▾</summary>
929
- <p></p>
419
+ ### Copy ActiveRecord Models
930
420
 
931
- ```ruby
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
- # force load the relation
936
- relation.load
937
- relation.loaded? # true
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
- encoded = URI::UID.build(relation).to_s
940
- decoded = URI::UID.parse(encoded).decode
448
+ uid = URI::UID.parse(uri)
449
+ #=> #<URI::UID payload=G7kAIBylMxZa7MouY3gUqHKkIx3hk4s8NT5xW..., fingerprint=CwuAkscJf6hDYW1wYWlnbtf_ReuZnGWeG5MD>
941
450
 
942
- decoded.is_a? ActiveRecord::Relation # true
943
- decoded.loaded? # false
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
- ## SignedGlobalID
454
+ copy == campaign
455
+ #=> false
951
456
 
952
- Features like `signing` _(to prevent tampering)_, `purpose`, and `expiration` are provided by SignedGlobalIDs.
953
- These features _(and more)_ will eventually be added to UniversalID, but until then...
954
- simply convert your UniversalID to a SignedGlobalID to add these features to any Universal ID.
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
- <details>
957
- <summary><b>How to Convert a UID to/from a SignedGlobalID</b>... ▾</summary>
958
- <p></p>
464
+ # create the copy (new records) in the database
465
+ copy.save #=> true
466
+ ```
959
467
 
960
- ```ruby
961
- product = {
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.
966
470
 
967
- uid = URI::UID.build(product)
968
- #=> #<URI::UID scheme="uid", host="universal-id", path="/G0sAgBypU587HsjkLpEnGHiaWfPQEyiiuH6j...">
471
+ ```ruby
472
+ packed = UniversalID::Packer.pack(campaign, options)
473
+ copy = UniversalID::Packer.unpack(packed)
474
+ copy.save
475
+ ```
969
476
 
970
- sgid = uid.to_sgid_param(for: "cart-123", expires_in: 1.hour)
971
- #=> "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJZ0d4WjJsa09pOHZkVzVwZG1WeWMyRnNMV2xrTDFWU1NUbzZWVWxFT2pwSGJHOWlZV3hKUkZKbFkyOXlaQzlITUhO..."
477
+ ### ActiveRecord::Relations
972
478
 
973
- URI::UID.from_sgid(sgid, for: "cart-123").decode
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
- # mismatched purpose returns nil... as expected
982
- URI::UID.from_sgid(sgid, for: "mismatch")
983
- #=> nil
984
- ```
985
- </details>
485
+ uri = URI::UID.build(relation).to_s
486
+ #=> "uid://universalid/G90EQCwLeEP1oQtHFksrdN5YS4ju5TryFZwBJgh2toqS3SKEVSl1FoNtZjI..."
986
487
 
987
- ## Fingerprinting (Implicit Versioning)
488
+ uid = URI::UID.parse(encoded)
489
+ #=> #<URI::UID payload=G90EQCwLeEP1oQtHFksrdN5YS4ju5TryFZwBJ..., fingerprint=CxKAkscXf7ZBY3RpdmVSZWNvcmQ6OlJlbGF0a...>
988
490
 
989
- Fingerprinting adds an extra layer of intelligence to the serialization process.
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
- Fingerprints are comprised of the following components:
493
+ # introspection
494
+ decoded == relation #=> true
495
+ decoded.is_a? ActiveRecord::Relation #=> true
496
+ decoded.loaded? #=> false
994
497
 
995
- 1. `Class (Class)` - The encoded object's class
996
- 2. `Timestamp (Time)` - The mtime (UTC) of the file that defined the object's class
498
+ # run the query
499
+ campaigns = decoded.load
500
+ ```
997
501
 
998
- > :bulb: **Modification Timestamp**: The `mtime` is detected and captured the moment a UID is created.
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
- Fingerprints allow developers to manage different versions of serialized data effectively...**without the need for custom versioning**.
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
- <details>
1005
- <summary><b>How to Use Fingerprinting</b>... ▾</summary>
1006
- <p></p>
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
- 1. Build a UID using a custom handler _(optional Ruby block)_. This allows you to take control of the encoding process.
511
+ ```ruby
512
+ data = OpenStruct.new(name: "Demo", value: "Example")
1009
513
 
1010
- ```ruby
1011
- # NOTE: The campaign model instance was setup earlier in the "Model Instances" section above
1012
- campaign.save!
514
+ sgid = URI::UID.build(data).to_sgid_param(for: "purpose", expires_in: 1.hour)
515
+ #=> "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaEpJZ0plQTJkcFpEb3ZMM1Z1YVhabGNuTmhiQzFwWkM5V..."
1013
516
 
1014
- # the uid build target (campaign in this case)
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
- data = { id: record.id, demo: true }
520
+ decoded = uid.decode
521
+ #=> #<OpenStruct name="Demo", value="Example">
1023
522
 
1024
- URI::UID.encode data, options.merge(include: %w[id demo]) # block returns the encoded payload
1025
- end
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