clusterid 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0b662076bc46a6188f0d587292d36cae6fb54b179d4b5e9077b114c8c36b6c1a
4
+ data.tar.gz: 9135abab52b8494ee37f0958d34dbceb9122e3b1babe6e5aa0113d76addb0d80
5
+ SHA512:
6
+ metadata.gz: 262e9dfefed1d936a8f5d406dc2d3d1cd0df8bf3508a4d72ba9cc085adc897035f5e93717594cffabf47289eaeff9d90d0923b195d4a1aa7961affad5ba54867
7
+ data.tar.gz: eed23a12338f989ca94729a64179a6965bb7b3fc8cfa4db6608a766b77ace168bfe6b2d6bced72e68eee86d4669e6e68071a0f96b4420768854b047d0d102043
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ # Changelog
2
+
3
+ ## v1.0.0 - 2022-02-26
4
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Stephan Tarulli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,283 @@
1
+ # ClusterID
2
+ Create unique identifiers for entities in distributed systems.
3
+
4
+ [![Gem Version](https://badge.fury.io/rb/clusterid.svg)](https://badge.fury.io/rb/clusterid)
5
+
6
+ ## What's in the Box?
7
+ ✅ Simple usage documentation written to get started fast. [Check it out!](#usage)
8
+
9
+ ⚡ A fast implementation in pure ruby. [Check it out!](#benchmarks)
10
+
11
+ 📚 YARD generated API documentation for the library. [Check it out!](https://tinychameleon.github.io/clusterid/)
12
+
13
+ 🤖 RBS types for your type checking wants. [Check it out!](./sig/clusterid.rbs)
14
+
15
+ 💎 Tests against many Ruby versions. [Check it out!](#ruby-versions)
16
+
17
+ 🔒 MFA protection on all gem owners. [Check it out!](https://rubygems.org/gems/clusterid)
18
+
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ ```ruby
25
+ gem 'clusterid'
26
+ ```
27
+
28
+ And then execute:
29
+
30
+ $ bundle install
31
+
32
+ Or install it yourself as:
33
+
34
+ $ gem install clusterid
35
+
36
+ ### Ruby Versions
37
+ This gem is tested against the following Ruby versions:
38
+
39
+ - 2.6.0
40
+ - 2.6.9
41
+ - 2.7.0
42
+ - 2.7.5
43
+ - 3.0.0
44
+ - 3.0.3
45
+ - 3.1.0
46
+ - 3.1.1
47
+
48
+
49
+ ## Usage
50
+ Create an intance of `ClusterId::V1::Generator` and then use the `generate` method to create `ClusterId::V1::Value` instances.
51
+
52
+ ```ruby
53
+ generator = ClusterId::V1::Generator.new
54
+ id = generator.generate
55
+ ```
56
+
57
+ This kind of quick usage will use the default byte generator, clock, and serialization classes. The default serialization
58
+ classes are null objects which will:
59
+
60
+ - return `nil` for deserializing any data centre, environment, or type ID
61
+ - serialize any value for data centre, environment, or type ID as `0`.
62
+
63
+ If you would like to take advantage of these custom attributes see the [Custom Attributes & Serialization section below](#custom-attributes--serialization).
64
+
65
+ ### Values
66
+ The `ClusterId::V1::Value` objects that are created have a small API which allows read access to the individual components
67
+ which make up the byte string. For example, to access the byte string for storage:
68
+
69
+ ```ruby
70
+ value = generator.generate
71
+ puts value.bytes
72
+ ```
73
+
74
+ [See the API docs for the full method reference](https://tinychameleon.github.io/clusterid/ClusterId/V1/Value.html).
75
+
76
+ ### Values are `Comparable`
77
+ The `ClusterId::V1::Value` class implements the `Comparable` interface, so you can sort them, check for equality, and use the typical comparison operators.
78
+
79
+ ### Reconstituting `ClusterId::V1::Value` Objects
80
+ If you've saved the byte string from one of these objects somewhere, you can manually create it again if you have a `ClusterId::V1::Deserializer`
81
+ or a `ClusterId::V1::Generator`.
82
+
83
+ With a generator configured with the correct `Deserializer`, you can use the `from_byte_string` method:
84
+
85
+ ```ruby
86
+ byte_string = "..." # From storage, perhaps.
87
+ generator = ... # Some existing generator with the correct configuration.
88
+ value = generator.from_byte_string(byte_string)
89
+ ```
90
+
91
+ Without a generator, you need to provide the `Deserializer` instance yourself:
92
+
93
+ ```ruby
94
+ byte_string = "..." # From storage, perhaps.
95
+ deserializer = ClusterId::V1::NullDeserializer.new
96
+ value = ClusterId::V1::Value.new(byte_string, deserializer)
97
+ ```
98
+
99
+ ### Custom Attributes & Serialization
100
+ You can create subclasses of `ClusterId::V1::Serializer` and `ClusterId::V1::Deserializer` to represent your custom values for data centre, environment, and type IDs.
101
+ The `Serializer` will need to accept your application's types for the custom attributes and return integer values, while the `Deserializer` should do the opposite.
102
+
103
+ You can use any type as long as you implement the serialization and deserialization steps. For example, here are two simple classes that use only `Symbols`.
104
+
105
+ ```ruby
106
+ class ExampleDeserializer < ClusterId::V1::Deserializer
107
+ def to_data_centre(i)
108
+ return :north_america if i == 1
109
+ :global
110
+ end
111
+
112
+ def to_environment(i)
113
+ return :production if i == 3
114
+ :non_production
115
+ end
116
+
117
+ def to_type_id(i)
118
+ return :user if i == 1
119
+ return :settings if i == 2
120
+ :unknown
121
+ end
122
+ end
123
+
124
+ class ExampleSerializer < ClusterId::V1::Serializer
125
+ def from_data_centre(s)
126
+ return 1 if s == :north_america
127
+ 2
128
+ end
129
+
130
+ def from_environment(s)
131
+ return 3 if s == :production
132
+ 1
133
+ end
134
+
135
+ def from_type_id(s)
136
+ return 1 if s == :user
137
+ return 2 if s == :settings
138
+ 99
139
+ end
140
+ end
141
+
142
+ # Then pass them to the Generator constructor
143
+ g = ClusterId::V1::Generator.new(serializer: ExampleSerializer.new, deserializer: ExampleDeserializer.new)
144
+ ```
145
+
146
+ Once you've implemented these custom classes you can provide your custom attributes to the `generate` method:
147
+
148
+ ```ruby
149
+ v = g.generate(data_centre: :north_america, environment: :production, type_id: :user)
150
+ puts v.type_id # Output: :user
151
+ ```
152
+
153
+ #### NOTE
154
+ When you implement custom serialization classes you become responsible for ensuring the bit length requirements for each
155
+ attribute are correct. [See the API documentation for details](https://tinychameleon.github.io/clusterid/ClusterId/V1/Value.html).
156
+
157
+ ### Custom Byte Generators
158
+ If you need to use a byte generator other than the default of `SecureRandom` you can implement your own.
159
+ The only requirement is to have a `bytes(n)` method which returns a byte string. For example:
160
+
161
+ ```ruby
162
+ module GuaranteedRandom
163
+ DICE_ROLL = [4].cycle
164
+
165
+ def bytes(n)
166
+ DICE_ROLL.take(n).pack("C")
167
+ end
168
+ end
169
+ ```
170
+
171
+ Once you have a custom byte generator you can provide it to the `ClusterId::V1::Generator` constructor:
172
+
173
+ ```ruby
174
+ g = ClusterId::V1::Generator(byte_generator: GuaranteedRandom)
175
+ ```
176
+
177
+ ### Custom Clock
178
+ If you need to implement a custom clock for millisecond timestamps you can implement your own.
179
+ The only requirement is to have a `now_ms` method which returns the millisecond time as an `Integer`.
180
+ For example:
181
+
182
+ ```ruby
183
+ module UltraAccurateClock
184
+ def now_ms
185
+ expensive_hardware.get_time_ms
186
+ end
187
+ end
188
+ ```
189
+
190
+ Once you have created a custom clock you can provide it to the `ClusterId::V1::Generator` constructor:
191
+
192
+ ```ruby
193
+ g = ClusterId::V1::Generator(clock: UltraAccurateClock)
194
+ ```
195
+
196
+
197
+ ## Contributing
198
+
199
+ ### Development
200
+ To get started development on this gem run the `bin/setup` command. This will install dependencies and run the tests and linting tasks to ensure everything is working.
201
+
202
+ For an interactive console with the gem loaded run `bin/console`.
203
+
204
+
205
+ ### Testing
206
+ Use the `bundle exec rake test` command to run unit tests. To install the gem onto your local machine for general integration testing use `bundle exec rake install`.
207
+
208
+ To test the gem against each supported version of Ruby use `bin/test_versions`. This will create a Docker image for each version and run the tests and linting steps.
209
+
210
+
211
+ ### Releasing
212
+ Do the following to release a new version of this gem:
213
+
214
+ - Update the version number in [lib/clusterid/version.rb](./lib/clusterid/version.rb)
215
+ - Ensure necessary documentation changes are complete
216
+ - Ensure changes are in the [CHANGELOG.md](./CHANGELOG.md)
217
+ - Create the new release using `bundle exec rake release`
218
+
219
+ After this is done the following side-effects should be visible:
220
+
221
+ - A new git tag for the version number should exist
222
+ - Commits for the new version should be pushed to GitHub
223
+ - The new gem should be available on [rubygems.org](https://rubygems.org).
224
+
225
+ Finally, update the documentation hosted on GitHub Pages:
226
+
227
+ - Check-out the `gh-pages` branch
228
+ - Merge `main` into the `gh-pages` branch
229
+ - Generate the documentation with `bundle exec rake yard`
230
+ - Commit the documentation on the `gh-pages` branch
231
+ - Push the new documentation so GitHub Pages can deploy it
232
+
233
+ ## Benchmarks
234
+ Benchmarking is tricky and the goal of a benchmark should be clear before attempting performance improvements. The goal of this library for performance is as follows:
235
+
236
+ > This library should be capable of generating new values and instantiating existing values at a rate which does not make it a bottleneck for the majority of web APIs.
237
+
238
+ Given the above goal statement, these benchmarks run on the following environment:
239
+
240
+ | Attribute | Value |
241
+ |:--|--:|
242
+ | Ruby Version | 3.1.0 |
243
+ | MacOS Version | Catalina 10.15.7 (19H1615) |
244
+ | MacOS Model Identifier | MacBookPro10,1 |
245
+ | MacOS Processor Name | Quad-Core Intel Core i7 |
246
+ | MacOS Processor Speed | 2.7 GHz |
247
+ | MacOS Number of Processors | 1 |
248
+ | MacOS Total Number of Cores | 4 |
249
+ | MacOS L2 Cache (per Core) | 256 KB |
250
+ | MacOS L3 Cache | 6 MB |
251
+ | MacOS Hyper-Threading Technology | Enabled |
252
+ | MacOS Memory | 16 GB |
253
+
254
+ ### `ClusterId::V1`
255
+ The performance is approximately as follows when run using:
256
+
257
+ - The default byte generator and clock
258
+ - Simple serializer and deserializer classes
259
+ - A constant 16 bytes of data for `Value` instantiation
260
+
261
+ ```
262
+ ~/…/clusterid› bundle exec rake benchmark
263
+ date; bundle exec ruby test/benchmarks/current.rb
264
+ Sat Feb 26 17:47:17 PST 2022
265
+ Warming up --------------------------------------
266
+ generate 23.924k i/100ms
267
+ from_byte_string 129.369k i/100ms
268
+ value 135.606k i/100ms
269
+ Calculating -------------------------------------
270
+ generate 241.890k (± 2.8%) i/s - 1.220M in 5.048294s
271
+ from_byte_string 1.270M (± 5.2%) i/s - 6.339M in 5.004944s
272
+ value 1.358M (± 6.2%) i/s - 6.780M in 5.014634s
273
+ ```
274
+
275
+ Being conservative in estimation and assuming:
276
+
277
+ - all nodes in a cluster have identical millisecond timestamp values
278
+ - 240 generated clusterid values per millisecond
279
+ - approximately 150,000 values generated for a 5 byte value yields a 1% collision rate
280
+
281
+ The `generate` method supports approximately 625 machines in a given cluster before the chance of a value collision reaches 1%.
282
+ The `value` instantiation has ample performance and should not be a bottleneck for generation of new values.
283
+ The `from_byte_string` API is nearly as fast as the value constructor and therefore should also not be a bottleneck.
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClusterId
4
+ # The expected byte size of {ClusterId} values.
5
+ BYTE_SIZE = 16
6
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClusterId
4
+ # The base error for all errors in {ClusterId}.
5
+ class Error < StandardError; end
6
+
7
+ # An error representing an invalid byte length for a value.
8
+ class InvalidByteLengthError < Error
9
+ # @param length [Integer] the byte length considered invalid
10
+ def initialize(length)
11
+ super("Expected #{BYTE_SIZE} bytes, got #{length}")
12
+ end
13
+ end
14
+
15
+ # An error representing an invalid data format version for a value.
16
+ class InvalidVersionError < Error
17
+ # @param expected [Integer] the expected data format version
18
+ # @param received [Integer] the received data format version
19
+ def initialize(expected, received)
20
+ super("Expected version #{expected}, got #{received}")
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClusterId
4
+ module V1
5
+ # A millisecond clock which extracts time information from the +Process+
6
+ # module.
7
+ class Clock
8
+ # @return [Integer] A millisecond timestamp since the Unix epoch
9
+ def self.now_ms
10
+ Process.clock_gettime Process::CLOCK_REALTIME, :millisecond
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module ClusterId
6
+ module V1
7
+ # Create instances of {Value} objects based on time, random data, and serializable
8
+ # values for data centre, environment, and type IDs.
9
+ class Generator
10
+ # @note It is the caller's responsibility to ensure the serializer and deserializer
11
+ # follow the expected bit lengths for custom types. No overflow checks are done.
12
+ # Overflow will either be discarded or overwrite other bits. See the {Value}
13
+ # class for allowed bit lengths.
14
+ #
15
+ # @param serializer [Serializer] an object which serializes {Value} attributes
16
+ # @param deserializer [Deserializer] an object which deserializes {Value} attributes
17
+ # @param byte_generator [#bytes(n)] an object which provides random bytes
18
+ # @param clock [#now_ms] an object which provides millisecond timestamps
19
+ def initialize(serializer: NullSerializer.new, deserializer: NullDeserializer.new, byte_generator: SecureRandom, clock: Clock)
20
+ @serializer = serializer
21
+ @deserializer = deserializer
22
+ @byte_generator = byte_generator
23
+ @clock = clock
24
+ end
25
+
26
+ # Generate a {Value} for a given data centre, environment, and type ID.
27
+ #
28
+ # @param data_centre [D] a serializable value representing the data centre the {Value} is generated within
29
+ # @param environment [E] a serializable value representing the environment the {Value} is generated within
30
+ # @param type_id [T] a serializable value representing the type of entity for a generated {Value}
31
+ # @return [Value] a time-based, partially random value to identify an entity
32
+ def generate(data_centre: nil, environment: nil, type_id: nil)
33
+ raw_data = +""
34
+ # Nonce
35
+ raw_data += @byte_generator.bytes(5)
36
+ # Type ID
37
+ raw_data += [@serializer.from_type_id(type_id)].pack("S")
38
+ # Details (Version, Data Centre, Environment)
39
+ raw_data += [
40
+ (FORMAT_VERSION << 5) +
41
+ (@serializer.from_data_centre(data_centre) << 2) +
42
+ @serializer.from_environment(environment)
43
+ ].pack("C")
44
+ # Timestamp
45
+ raw_data += [@clock.now_ms].pack("Q")
46
+
47
+ Value.new(raw_data.freeze, @deserializer)
48
+ end
49
+
50
+ # @return [Value] the {Value} represented by +s+
51
+ def from_byte_string(s)
52
+ Value.new(s, @deserializer)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClusterId
4
+ module V1
5
+ # A default {Deserializer} that returns +nil+ for all attributes.
6
+ class NullDeserializer < Deserializer
7
+ # Null method which ignores all data centre values.
8
+ # @return [nil]
9
+ def to_data_centre(_)
10
+ nil
11
+ end
12
+
13
+ # Null method which ignores all environment values.
14
+ # @return [nil]
15
+ def to_environment(_)
16
+ nil
17
+ end
18
+
19
+ # Null method which ignores all type ID values.
20
+ # @return [nil]
21
+ def to_type_id(_)
22
+ nil
23
+ end
24
+ end
25
+
26
+ # A default {Serializer} that returns +0+ for all attributes.
27
+ class NullSerializer < Serializer
28
+ # Zero method which ignores all data centre values.
29
+ # @return [0]
30
+ def from_data_centre(_)
31
+ 0
32
+ end
33
+
34
+ # Zero method which ignores all environment values.
35
+ # @return [0]
36
+ def from_environment(_)
37
+ 0
38
+ end
39
+
40
+ # Zero method which ignores all type ID values.
41
+ # @return [0]
42
+ def from_type_id(_)
43
+ 0
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClusterId
4
+ module V1
5
+ # The abstract base class of all custom serialization logic.
6
+ class Serializer
7
+ # @param t [T] the environment
8
+ # @return [Integer] the serialized environment
9
+ def from_environment(t)
10
+ raise NotImplementedError
11
+ end
12
+
13
+ # @param t [T] the data centre
14
+ # @return [Integer] the serialized data centre
15
+ def from_data_centre(t)
16
+ raise NotImplementedError
17
+ end
18
+
19
+ # @param t [T] the type ID
20
+ # @return [Integer] the serialized type ID
21
+ def from_type_id(t)
22
+ raise NotImplementedError
23
+ end
24
+ end
25
+
26
+ # The abstract base class of all custom deserializtion logic.
27
+ class Deserializer
28
+ # @param i [Integer] the serialized environment
29
+ # @return [T] the deserialized environment
30
+ def to_environment(i)
31
+ raise NotImplementedError
32
+ end
33
+
34
+ # @param i [Integer] the serialized data centre
35
+ # @return [T] the deserialized data centre
36
+ def to_data_centre(i)
37
+ raise NotImplementedError
38
+ end
39
+
40
+ # @param i [Integer] the serialized type ID
41
+ # @return [T] the deserialized type ID
42
+ def to_type_id(i)
43
+ raise NotImplementedError
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module ClusterId
6
+ # Data format version 1 for {ClusterId} identifiers. This format supports
7
+ # custom value serialization for data centre, environment, and type IDs.
8
+ #
9
+ # @see Value
10
+ # @see Generator
11
+ module V1
12
+ # The data format version
13
+ FORMAT_VERSION = 1
14
+
15
+ # A {ClusterId} version 1 format value.
16
+ #
17
+ # A {Value} contains the following accessible properties:
18
+ #
19
+ # - a 64-bit millisecond timestamp as a +DateTime+
20
+ # - a 3-bit version number
21
+ # - a 3-bit data centre identifier
22
+ # - a 2-bit environment identifier
23
+ # - a 16-bit type identifier
24
+ # - a 40-bit random nonce
25
+ #
26
+ # It also provides access to the underlying bytes.
27
+ #
28
+ # Instances of {Value} are +Comparable+ based on the timestamp.
29
+ #
30
+ # === Random Nonce
31
+ # A 5 byte random nonce supports generating approximately 150,000 values before
32
+ # reaching a 1% chance of collision. This applies to each millisecond.
33
+ #
34
+ # === Data Layout
35
+ # The byte layout of a version 1 value in little-endian is:
36
+ # o-------------------------------------------------------------------------------o
37
+ # | byte 01 | byte 02 | byte 03 | byte 04 | byte 05 | byte 06 | byte 07 | byte 08 |
38
+ # |-------------------------------------------------------------------------------|
39
+ # | random nonce | type id | details |
40
+ # |-------------------------------------------------------------------------------|
41
+ # | byte 09 | byte 10 | byte 11 | byte 12 | byte 13 | byte 14 | byte 15 | byte 16 |
42
+ # |-------------------------------------------------------------------------------|
43
+ # | timestamp |
44
+ # o-------------------------------------------------------------------------------o
45
+ # With the +details+ byte encoding the following data:
46
+ # o---------------------------------------------------------------o
47
+ # | bit 8 | bit 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 |
48
+ # |---------------------------------------------------------------|
49
+ # | version | data centre | environment |
50
+ # o---------------------------------------------------------------o
51
+ class Value
52
+ include Comparable
53
+
54
+ # @return [String] the underlying value bytes
55
+ attr_reader :bytes
56
+
57
+ # [RUBY 2.6][RUBY 2.7]
58
+ # Referencing undefined instance variables causes warnings to be emitted,
59
+ # so +instance_variable_defined?+ is used to determine memoization status.
60
+
61
+ # @return [DateTime] the value's creation datetime
62
+ def datetime
63
+ return @dt if instance_variable_defined? :@dt
64
+ @dt = DateTime.strptime(bytes[8..].unpack1("Q").to_s, "%Q")
65
+ end
66
+
67
+ # @return [Integer] the value's random nonce
68
+ def nonce
69
+ return @nonce if instance_variable_defined? :@nonce
70
+ @nonce = (bytes[0..4] + "\x00\x00\x00").unpack1("Q")
71
+ end
72
+
73
+ # @return [Integer] the value's version
74
+ def version
75
+ FORMAT_VERSION
76
+ end
77
+
78
+ # @return [T] the value's deserialized environment
79
+ def environment
80
+ return @env if instance_variable_defined? :@env
81
+ @env = @deserializer.to_environment bytes[7].unpack1("C") & 0x03
82
+ end
83
+
84
+ # @return [T] the value's deserialized data centre
85
+ def data_centre
86
+ return @dc if instance_variable_defined? :@dc
87
+ @dc = @deserializer.to_data_centre (bytes[7].unpack1("C") & 0x1c) >> 2
88
+ end
89
+
90
+ # @return [T] the value's deserialized type ID
91
+ def type_id
92
+ return @tid if instance_variable_defined? :@tid
93
+ @tid = @deserializer.to_type_id bytes[5..6].unpack1("S")
94
+ end
95
+
96
+ # @param bytes [String] the value as a byte string
97
+ # @param deserializer [Deserializer] a {Deserializer} to decode custom value attributes
98
+ # @raise [InvalidByteLengthError] when the length of bytes is not {BYTE_SIZE}
99
+ # @raise [InvalidVersionError] when the data format version is not {FORMAT_VERSION}
100
+ def initialize(bytes, deserializer)
101
+ raise InvalidByteLengthError, bytes.length unless bytes.length == BYTE_SIZE
102
+
103
+ version = (bytes[7].unpack1("C") & 0xe0) >> 5
104
+ raise InvalidVersionError.new(FORMAT_VERSION, version) unless version == FORMAT_VERSION
105
+
106
+ @bytes = bytes
107
+ @deserializer = deserializer
108
+ end
109
+
110
+ # Compares {Value} objects using {#datetime}
111
+ #
112
+ # @param other [Value] the object to compare against
113
+ # @return [-1, 0, 1] -1 when less than +other+, 0 when equal to +other+, 1 when greater than +other+
114
+ def <=>(other)
115
+ datetime <=> other.datetime
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Create unique identifiers for entities in distributed systems.
4
+ # @since 1.0.0
5
+ module ClusterId
6
+ # The gem version.
7
+ VERSION = "1.0.0"
8
+ end
data/lib/clusterid.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "clusterid/version"
4
+ require_relative "clusterid/constants"
5
+ require_relative "clusterid/errors"
6
+
7
+ require_relative "clusterid/v1/serialization"
8
+ require_relative "clusterid/v1/null_serialization"
9
+ require_relative "clusterid/v1/clock"
10
+ require_relative "clusterid/v1/value"
11
+ require_relative "clusterid/v1/generator"
data/sig/clusterid.rbs ADDED
@@ -0,0 +1,67 @@
1
+ module ClusterId
2
+ VERSION: String
3
+ BYTE_SIZE: 16
4
+
5
+ class Error < StandardError
6
+ end
7
+
8
+ class InvalidByteLengthError < Error
9
+ def initialize: (Integer) -> void
10
+ end
11
+
12
+ class InvalidVersionError < Error
13
+ def initialize: (Integer, Integer) -> void
14
+ end
15
+
16
+ module V1
17
+ FORMAT_VERSION: 1
18
+
19
+ class Deserializer[D, E, T]
20
+ def to_data_centre: (Integer) -> D
21
+ def to_environment: (Integer) -> E
22
+ def to_type_id: (Integer) -> T
23
+ end
24
+
25
+ class Serializer[D, E, T]
26
+ def from_data_centre: (D) -> Integer
27
+ def from_environment: (E) -> Integer
28
+ def from_type_id: (T) -> Integer
29
+ end
30
+
31
+ interface _ByteSource
32
+ def bytes: (Integer) -> String
33
+ end
34
+
35
+ interface _Clock
36
+ def now_ms: () -> Integer
37
+ end
38
+
39
+ class Value[D, E, T]
40
+ attr_reader bytes: String
41
+ def datetime: () -> DateTime
42
+ def nonce: () -> Integer
43
+ def version: () -> Integer
44
+ def environment: () -> E
45
+ def data_centre: () -> D
46
+ def type_id: () -> T
47
+
48
+ def initialize: (String, Deserializer[D, E, T]) -> void
49
+ end
50
+
51
+ class Generator[D, E, T]
52
+ def generate: (data_centre: D, environment: E, type_id: T) -> Value[D, E, T]
53
+
54
+ def initialize: (?serializer: Serializer[D, E, T], ?deserializer: Deserializer[D, E, T], ?byte_generator: _ByteSource, ?clock: _Clock) -> void
55
+ end
56
+
57
+ class Clock
58
+ include _Clock
59
+ end
60
+
61
+ class NullSerializer < Serializer[top, top, top]
62
+ end
63
+
64
+ class NullDeserializer < Deserializer[nil, nil, nil]
65
+ end
66
+ end
67
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: clusterid
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Stephan Tarulli
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-02-27 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Create unique identifiers for entities in distributed systems
14
+ email:
15
+ - srt@tinychameleon.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - CHANGELOG.md
21
+ - LICENSE.txt
22
+ - README.md
23
+ - lib/clusterid.rb
24
+ - lib/clusterid/constants.rb
25
+ - lib/clusterid/errors.rb
26
+ - lib/clusterid/v1/clock.rb
27
+ - lib/clusterid/v1/generator.rb
28
+ - lib/clusterid/v1/null_serialization.rb
29
+ - lib/clusterid/v1/serialization.rb
30
+ - lib/clusterid/v1/value.rb
31
+ - lib/clusterid/version.rb
32
+ - sig/clusterid.rbs
33
+ homepage: https://github.com/tinychameleon/clusterid
34
+ licenses:
35
+ - MIT
36
+ metadata:
37
+ homepage_uri: https://github.com/tinychameleon/clusterid
38
+ source_code_uri: https://github.com/tinychameleon/clusterid
39
+ changelog_uri: https://github.com/tinychameleon/clusterid/blob/main/CHANGELOG.md
40
+ bug_tracker_uri: https://github.com/tinychameleon/clusterid/issues
41
+ documentation_uri: https://tinychameleon.github.com/clusterid/
42
+ post_install_message:
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 2.6.0
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubygems_version: 3.3.3
58
+ signing_key:
59
+ specification_version: 4
60
+ summary: Create unique identifiers for entities in distributed systems
61
+ test_files: []