ruby-ulid 0.0.11 → 0.0.16
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 +4 -4
- data/README.md +175 -52
- data/lib/ulid.rb +171 -21
- data/lib/ulid/monotonic_generator.rb +15 -13
- data/lib/ulid/version.rb +1 -1
- data/sig/ulid.rbs +23 -7
- metadata +21 -8
- data/Steepfile +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 15a83604732cdc37f8015ee9382884e950852f8c9c82aba107ea4b3c065c5a4d
|
4
|
+
data.tar.gz: 06e7b69a5838786f4d23da19f5e1f00fb3173014e7438459165b66703637851a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ca46525e80b1d832a703b8cb867466327de333853ee236654f722dc6abc30f46fd149ca9633c42ececcea08db2a01a6d82ab60f8eeb61ebb947f0441436dd96
|
7
|
+
data.tar.gz: ccd08e78dfb047f82af02eeaf14ccd12fcb05fe882edeb2bf48c2e7ec5b9fd698feccf0b9f9e187aaff97c2e793f671a3f9d32c11e3741656c3df94f944bc808
|
data/README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# ruby-ulid
|
2
2
|
|
3
|
-
|
3
|
+
## Overview
|
4
4
|
|
5
|
-
The `ULID` spec is defined on [ulid/spec](https://github.com/ulid/spec).
|
5
|
+
The `ULID` spec is defined on [ulid/spec](https://github.com/ulid/spec). It has useful specs for applications (e.g. `Database key`), especially possess all `uniqueness`, `randomness`, `extractable timestamps` and `sortable` features.
|
6
6
|
This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around the ULID.
|
7
|
-
Also providing rbs signature files.
|
7
|
+
Also providing [ruby/rbs](https://github.com/ruby/rbs) signature files.
|
8
8
|
|
9
9
|
---
|
10
10
|
|
@@ -28,19 +28,31 @@ Instead, herein is proposed ULID:
|
|
28
28
|
- 1.21e+24 unique ULIDs per millisecond
|
29
29
|
- Lexicographically sortable!
|
30
30
|
- Canonically encoded as a 26 character string, as opposed to the 36 character UUID
|
31
|
-
- Uses Crockford's base32 for better efficiency and readability (5 bits per character)
|
31
|
+
- Uses [Crockford's base32](https://www.crockford.com/base32.html) for better efficiency and readability (5 bits per character) # See also exists issues in [Note](#note)
|
32
32
|
- Case insensitive
|
33
33
|
- No special characters (URL safe)
|
34
34
|
- Monotonic sort order (correctly detects and handles the same millisecond)
|
35
35
|
|
36
|
-
##
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
### Install
|
39
|
+
|
40
|
+
Require Ruby 2.6 or later
|
41
|
+
|
42
|
+
This command will install the latest version into your environment
|
37
43
|
|
38
44
|
```console
|
39
45
|
$ gem install ruby-ulid
|
40
|
-
|
46
|
+
Should be installed!
|
41
47
|
```
|
42
48
|
|
43
|
-
|
49
|
+
Add this line to your application/library's `Gemfile` is needed in basic use-case
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
gem 'ruby-ulid', '0.0.16'
|
53
|
+
```
|
54
|
+
|
55
|
+
### Generator and Parser
|
44
56
|
|
45
57
|
The generated `ULID` is an object not just a string.
|
46
58
|
It means easily get the timestamps and binary formats.
|
@@ -52,36 +64,17 @@ ulid = ULID.generate #=> ULID(2021-04-27 17:27:22.826 UTC: 01F4A5Y1YAQCYAYCTC7GR
|
|
52
64
|
ulid.to_time #=> 2021-04-27 17:27:22.826 UTC
|
53
65
|
ulid.to_s #=> "01F4A5Y1YAQCYAYCTC7GRMJ9AA"
|
54
66
|
ulid.octets #=> [1, 121, 20, 95, 7, 202, 187, 60, 175, 51, 76, 60, 49, 73, 37, 74]
|
55
|
-
ulid.pattern #=> /(?<timestamp>01F4A5Y1YA)(?<randomness>QCYAYCTC7GRMJ9AA)/i
|
56
|
-
```
|
57
|
-
|
58
|
-
Generator can take `Time` instance
|
59
|
-
|
60
|
-
```ruby
|
61
|
-
time = Time.at(946684800, in: 'UTC') #=> 2000-01-01 00:00:00 UTC
|
62
|
-
ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P)
|
63
|
-
ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT3JTMN0T14EBP)
|
64
|
-
|
65
|
-
ulids = 1000.times.map do
|
66
|
-
ULID.generate(moment: time)
|
67
|
-
end
|
68
|
-
ulids.sort == ulids #=> false
|
69
|
-
|
70
|
-
ulids = 1000.times.map do |n|
|
71
|
-
ULID.generate(moment: time + n)
|
72
|
-
end
|
73
|
-
ulids.sort == ulids #=> true
|
74
67
|
```
|
75
68
|
|
76
|
-
You can
|
77
|
-
|
78
|
-
FYI: Current parser/validator/matcher implementation aims `strict`, It might be changed in [ulid/spec#57](https://github.com/ulid/spec/pull/57) and [ruby-ulid#57](https://github.com/kachick/ruby-ulid/issues/57).
|
69
|
+
You can get the objects from exists encoded ULIDs
|
79
70
|
|
80
71
|
```ruby
|
81
72
|
ulid = ULID.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV') #=> ULID(2016-07-30 23:54:10.259 UTC: 01ARZ3NDEKTSV4RRFFQ69G5FAV)
|
82
73
|
ulid.to_time #=> 2016-07-30 23:54:10.259 UTC
|
83
74
|
```
|
84
75
|
|
76
|
+
### Sortable with the timestamp
|
77
|
+
|
85
78
|
ULIDs are sortable when they are generated in different timestamp with milliseconds precision
|
86
79
|
|
87
80
|
```ruby
|
@@ -89,8 +82,21 @@ ulids = 1000.times.map do
|
|
89
82
|
sleep(0.001)
|
90
83
|
ULID.generate
|
91
84
|
end
|
92
|
-
ulids.sort == ulids #=> true
|
93
85
|
ulids.uniq(&:to_time).size #=> 1000
|
86
|
+
ulids.sort == ulids #=> true
|
87
|
+
```
|
88
|
+
|
89
|
+
`ULID.generate` can take fixed `Time` instance
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
time = Time.at(946684800).utc #=> 2000-01-01 00:00:00 UTC
|
93
|
+
ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB00N018DCPJA4H9379P)
|
94
|
+
ULID.generate(moment: time) #=> ULID(2000-01-01 00:00:00.000 UTC: 00VHNCZB006WQT3JTMN0T14EBP)
|
95
|
+
|
96
|
+
ulids = 1000.times.map do |n|
|
97
|
+
ULID.generate(moment: time + n)
|
98
|
+
end
|
99
|
+
ulids.sort == ulids #=> true
|
94
100
|
```
|
95
101
|
|
96
102
|
The basic generator prefers `randomness`, it does not guarantee `sortable` for same milliseconds ULIDs.
|
@@ -103,32 +109,72 @@ ulids.uniq(&:to_time).size #=> 35 (the size is not fixed, might be changed in en
|
|
103
109
|
ulids.sort == ulids #=> false
|
104
110
|
```
|
105
111
|
|
106
|
-
|
112
|
+
### How to keep `Sortable` even if in same timestamp
|
113
|
+
|
114
|
+
If you want to prefer `sortable`, Use `MonotonicGenerator` instead. It is called as [Monotonicity](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#monotonicity) on the spec.
|
107
115
|
(Though it starts with new random value when changed the timestamp)
|
108
116
|
|
109
117
|
```ruby
|
110
118
|
monotonic_generator = ULID::MonotonicGenerator.new
|
111
|
-
|
119
|
+
ulids = 10000.times.map do
|
112
120
|
monotonic_generator.generate
|
113
121
|
end
|
114
|
-
sample_ulids_by_the_time =
|
115
|
-
sample_ulids_by_the_time.size #=>
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
122
|
+
sample_ulids_by_the_time = ulids.uniq(&:to_time)
|
123
|
+
sample_ulids_by_the_time.size #=> 32 (the size is not fixed, might be changed in environment)
|
124
|
+
|
125
|
+
# In same milliseconds creation, it just increments the end of randomness part
|
126
|
+
ulids.take(5) #=>
|
127
|
+
# [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
|
128
|
+
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK5),
|
129
|
+
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK6),
|
130
|
+
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK7),
|
131
|
+
# ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK8)]
|
132
|
+
|
133
|
+
# When the milliseconds is updated, it starts with new randomness
|
134
|
+
sample_ulids_by_the_time.take(5) #=>
|
135
|
+
# [ULID(2021-05-02 15:23:48.917 UTC: 01F4PTVCSN9ZPFKYTY2DDJVRK4),
|
136
|
+
# ULID(2021-05-02 15:23:48.918 UTC: 01F4PTVCSPF2KXG4ABT7CK3204),
|
137
|
+
# ULID(2021-05-02 15:23:48.919 UTC: 01F4PTVCSQF1GERBPCQV6TCX2K),
|
138
|
+
# ULID(2021-05-02 15:23:48.920 UTC: 01F4PTVCSRBXN2H4P1EYWZ27AK),
|
139
|
+
# ULID(2021-05-02 15:23:48.921 UTC: 01F4PTVCSSK0ASBBZARV7013F8)]
|
140
|
+
|
141
|
+
ulids.sort == ulids #=> true
|
142
|
+
```
|
143
|
+
|
144
|
+
### Filtering IDs with `Time`
|
145
|
+
|
146
|
+
`ULID` can be element of the `Range`. If you generated the IDs in monotonic generator, ID based filtering is easy and reliable
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
include_end = ulid1..ulid2
|
150
|
+
exclude_end = ulid1...ulid2
|
151
|
+
|
152
|
+
ulids.grep(one_of_the_above)
|
153
|
+
ulids.grep_v(one_of_the_above)
|
154
|
+
```
|
155
|
+
|
156
|
+
When want to filter ULIDs with `Time`, we should consider to handle the precision.
|
157
|
+
So this gem provides `ULID.range` to generate reasonable `Range[ULID]` from `Range[Time]`
|
128
158
|
|
129
|
-
|
159
|
+
```ruby
|
160
|
+
# Both of below, The begin of `Range[ULID]` will be the minimum in the floored milliseconds of the time1
|
161
|
+
include_end = ULID.range(time1..time2) #=> The end of `Range[ULID]` will be the maximum in the floored milliseconds of the time2
|
162
|
+
exclude_end = ULID.range(time1...time2) #=> The end of `Range[ULID]` will be the minimum in the floored milliseconds of the time2
|
163
|
+
|
164
|
+
# Below patterns are acceptable
|
165
|
+
pinpointing = ULID.range(time1..time1) #=> This will match only for all IDs in `time1`
|
166
|
+
until_the_end = ULID.range(..time1) #=> This will match only for all IDs upto `time1` (The `nil` starting `Range` can be used since Ruby 2.7)
|
167
|
+
until_the_end = ULID.range(ULID.min.to_time..time1) #=> This is same as above for Ruby 2.6
|
168
|
+
until_the_ulid_limit = ULID.range(time1..) # This will match only for all IDs from `time1` to max value of the ULID limit
|
169
|
+
|
170
|
+
# So you can use the generated range objects as below
|
171
|
+
ulids.grep(one_of_the_above)
|
172
|
+
ulids.grep_v(one_of_the_above)
|
173
|
+
#=> I hope the results should be actually you want!
|
130
174
|
```
|
131
175
|
|
176
|
+
### Scanner for string (e.g. `JSON`)
|
177
|
+
|
132
178
|
For rough operations, `ULID.scan` might be useful.
|
133
179
|
|
134
180
|
```ruby
|
@@ -169,6 +215,8 @@ ULID.scan(json).to_a
|
|
169
215
|
ULID(2021-04-30 05:53:12.478 UTC: 01F4GND4RYYSKNAADHQ9BNXAWJ)]
|
170
216
|
```
|
171
217
|
|
218
|
+
### Some methods to help manipulations
|
219
|
+
|
172
220
|
`ULID.min` and `ULID.max` return termination values for ULID spec.
|
173
221
|
|
174
222
|
```ruby
|
@@ -180,7 +228,10 @@ ULID.min(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3V000000000
|
|
180
228
|
ULID.max(moment: time) #=> ULID(2000-01-01 00:00:00.123 UTC: 00VHNCZB3VZZZZZZZZZZZZZZZZ)
|
181
229
|
```
|
182
230
|
|
183
|
-
`ULID#next` and `ULID#succ` returns next(successor) ULID
|
231
|
+
`ULID#next` and `ULID#succ` returns next(successor) ULID.
|
232
|
+
Especially `ULID#succ` makes it possible `Range[ULID]#each`.
|
233
|
+
|
234
|
+
NOTE: But basically `Range[ULID]#each` should not be used, incrementing 128 bits IDs are not reasonable operation in most case
|
184
235
|
|
185
236
|
```ruby
|
186
237
|
ULID.parse('01BX5ZZKBKZZZZZZZZZZZZZZZY').next.to_s #=> "01BX5ZZKBKZZZZZZZZZZZZZZZZ"
|
@@ -196,15 +247,87 @@ ULID.parse('01BX5ZZKBK0000000000000000').pred.to_s #=> "01BX5ZZKBJZZZZZZZZZZZZZZ
|
|
196
247
|
ULID.parse('00000000000000000000000000').pred #=> nil
|
197
248
|
```
|
198
249
|
|
199
|
-
UUIDv4 converter for migration use-cases
|
250
|
+
### UUIDv4 converter for migration use-cases
|
251
|
+
|
252
|
+
`ULID.from_uuidv4` and `ULID#to_uuidv4` is the converter.
|
253
|
+
The imported timestamp is meaningless. So ULID's benefit will lost
|
200
254
|
|
201
255
|
```ruby
|
202
|
-
|
203
|
-
#=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
|
256
|
+
# Basically reversible
|
257
|
+
ulid = ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS)
|
258
|
+
ulid.to_uuidv4 #=> "0983d0a2-ff15-4d83-8f37-7dd945b5aa39"
|
259
|
+
|
260
|
+
uuid_v4s = 10000.times.map { SecureRandom.uuid }
|
261
|
+
uuid_v4s.uniq.size == 10000 #=> Probably `true`
|
262
|
+
|
263
|
+
ulids = uuid_v4s.map { |uuid_v4| ULID.from_uuidv4(uuid_v4) }
|
264
|
+
ulids.map(&:to_uuidv4) == uuid_v4s #=> **Probably** `true` except below examples.
|
265
|
+
|
266
|
+
# NOTE: Some boundary values are not reversible. See below.
|
267
|
+
|
268
|
+
ULID.min.to_uuidv4 #=> "00000000-0000-4000-8000-000000000000"
|
269
|
+
ULID.max.to_uuidv4 #=> "ffffffff-ffff-4fff-bfff-ffffffffffff"
|
270
|
+
|
271
|
+
# These importing results are same as https://github.com/ahawker/ulid/tree/96bdb1daad7ce96f6db8c91ac0410b66d2e1c4c1 on CPython 3.9.4
|
272
|
+
reversed_min = ULID.from_uuidv4('00000000-0000-4000-8000-000000000000') #=> ULID(1970-01-01 00:00:00.000 UTC: 00000000008008000000000000)
|
273
|
+
reversed_max = ULID.from_uuidv4('ffffffff-ffff-4fff-bfff-ffffffffffff') #=> ULID(10889-08-02 05:31:50.655 UTC: 7ZZZZZZZZZ9ZZVZZZZZZZZZZZZ)
|
274
|
+
|
275
|
+
# But they are not reversible! Need to consider this issue in https://github.com/kachick/ruby-ulid/issues/76
|
276
|
+
ULID.min == reversed_min #=> false
|
277
|
+
ULID.max == reversed_max #=> false
|
278
|
+
```
|
279
|
+
|
280
|
+
## How to migrate from other gems
|
281
|
+
|
282
|
+
As far as I know, major prior arts are below
|
283
|
+
|
284
|
+
### [ulid gem](https://rubygems.org/gems/ulid) - [rafaelsales/ulid](https://github.com/rafaelsales/ulid)
|
285
|
+
|
286
|
+
It is just providing basic `String` generator only.
|
287
|
+
So you can replace the code as below
|
288
|
+
|
289
|
+
```diff
|
290
|
+
-ULID.generate
|
291
|
+
+ULID.generate.to_s
|
204
292
|
```
|
205
293
|
|
294
|
+
NOTE: It had crucial issue for handling precision, in version before `1.3.0`, when you extract timestamps from old generated ULIDs, it might be not accurate value.
|
295
|
+
|
296
|
+
1. [Sort order does not respect millisecond ordering](https://github.com/rafaelsales/ulid/issues/22)
|
297
|
+
1. [Fixed in this PR](https://github.com/rafaelsales/ulid/pull/23)
|
298
|
+
1. [Released in 1.3.0](https://github.com/rafaelsales/ulid/compare/1.2.0...v1.3.0)
|
299
|
+
|
300
|
+
### [ulid-ruby gem](https://rubygems.org/gems/ulid-ruby) - [abachman/ulid-ruby](https://github.com/abachman/ulid-ruby)
|
301
|
+
|
302
|
+
It is providing basic generator(except monotonic generator) and parser.
|
303
|
+
Major methods can be replaced as below.
|
304
|
+
|
305
|
+
```diff
|
306
|
+
-ULID.generate
|
307
|
+
+ULID.generate.to_s
|
308
|
+
-ULID.at(time)
|
309
|
+
+ULID.generate(moment: time).to_s
|
310
|
+
-ULID.time(string)
|
311
|
+
+ULID.parse(string).to_time
|
312
|
+
-ULID.min_ulid_at(time)
|
313
|
+
+ULID.min(moment: time).to_s
|
314
|
+
-ULID.max_ulid_at(time)
|
315
|
+
+ULID.max(moment: time).to_s
|
316
|
+
```
|
317
|
+
|
318
|
+
NOTE: It is still having precision issue similar as `ulid gem` in the both generator and parser. I sent PRs.
|
319
|
+
|
320
|
+
1. [Parsed time object has more than milliseconds](https://github.com/abachman/ulid-ruby/issues/3)
|
321
|
+
1. [Fix to handle timestamp precision in parser](https://github.com/abachman/ulid-ruby/pull/5)
|
322
|
+
1. [Fix to handle timestamp precision in generator](https://github.com/abachman/ulid-ruby/pull/4)
|
323
|
+
|
206
324
|
## References
|
207
325
|
|
326
|
+
- [Repository](https://github.com/kachick/ruby-ulid)
|
208
327
|
- [API documents](https://kachick.github.io/ruby-ulid/)
|
209
328
|
- [ulid/spec](https://github.com/ulid/spec)
|
210
|
-
|
329
|
+
|
330
|
+
## Note
|
331
|
+
|
332
|
+
- Another choices for sortable and randomness IDs, [UUIDv6, UUIDv7, UUIDv8 might be the one. (But they are still in draft state)](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-01.html), I will track them in [ruby-ulid#37](https://github.com/kachick/ruby-ulid/issues/37)
|
333
|
+
- Current parser/validator/matcher aims to cover `subset of Crockford's base32`. Suggesting it in [ulid/spec#57](https://github.com/ulid/spec/pull/57). Be that as it may, I might provide special handler or converter for the exception in [ruby-ulid#57](https://github.com/kachick/ruby-ulid/issues/57) and/or [ruby-ulid#78](https://github.com/kachick/ruby-ulid/issues/78)
|
data/lib/ulid.rb
CHANGED
@@ -16,6 +16,7 @@ class ULID
|
|
16
16
|
class Error < StandardError; end
|
17
17
|
class OverflowError < Error; end
|
18
18
|
class ParserError < Error; end
|
19
|
+
class SetupError < ScriptError; end
|
19
20
|
|
20
21
|
encoding_string = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
|
21
22
|
# Crockford's Base32. Excluded I, L, O, U.
|
@@ -35,7 +36,7 @@ class ULID
|
|
35
36
|
STRICT_PATTERN = /\A#{PATTERN.source}\z/i.freeze
|
36
37
|
|
37
38
|
# Imported from https://stackoverflow.com/a/38191104/1212807, thank you!
|
38
|
-
UUIDV4_PATTERN = /\A[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\z/i
|
39
|
+
UUIDV4_PATTERN = /\A[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\z/i.freeze
|
39
40
|
|
40
41
|
# Same as Time#inspect since Ruby 2.7, just to keep backward compatibility
|
41
42
|
# @see https://bugs.ruby-lang.org/issues/15958
|
@@ -45,20 +46,19 @@ class ULID
|
|
45
46
|
# @param [Integer] entropy
|
46
47
|
# @return [ULID]
|
47
48
|
def self.generate(moment: current_milliseconds, entropy: reasonable_entropy)
|
48
|
-
milliseconds
|
49
|
-
new milliseconds: milliseconds, entropy: entropy
|
49
|
+
new milliseconds: milliseconds_from_moment(moment), entropy: entropy
|
50
50
|
end
|
51
51
|
|
52
52
|
# @param [Integer, Time] moment
|
53
53
|
# @return [ULID]
|
54
54
|
def self.min(moment: 0)
|
55
|
-
generate(moment: moment, entropy: 0)
|
55
|
+
0.equal?(moment) ? MIN : generate(moment: moment, entropy: 0)
|
56
56
|
end
|
57
57
|
|
58
58
|
# @param [Integer, Time] moment
|
59
59
|
# @return [ULID]
|
60
60
|
def self.max(moment: MAX_MILLISECONDS)
|
61
|
-
generate(moment: moment, entropy: MAX_ENTROPY)
|
61
|
+
MAX_MILLISECONDS.equal?(moment) ? MAX : generate(moment: moment, entropy: MAX_ENTROPY)
|
62
62
|
end
|
63
63
|
|
64
64
|
# @deprecated This method actually changes class state. Use {ULID::MonotonicGenerator} instead.
|
@@ -122,27 +122,82 @@ class ULID
|
|
122
122
|
new milliseconds: milliseconds, entropy: entropy
|
123
123
|
end
|
124
124
|
|
125
|
+
# @param [Range<Time>, Range<nil>] time_range
|
126
|
+
# @return [Range<ULID>]
|
127
|
+
# @raise [ArgumentError] if the given time_range is not a `Range[Time]` or `Range[nil]`
|
128
|
+
def self.range(time_range)
|
129
|
+
raise argument_error_for_range_building(time_range) unless time_range.kind_of?(Range)
|
130
|
+
begin_time, end_time, exclude_end = time_range.begin, time_range.end, time_range.exclude_end?
|
131
|
+
|
132
|
+
case begin_time
|
133
|
+
when Time
|
134
|
+
begin_ulid = min(moment: begin_time)
|
135
|
+
when nil
|
136
|
+
begin_ulid = min
|
137
|
+
else
|
138
|
+
raise argument_error_for_range_building(time_range)
|
139
|
+
end
|
140
|
+
|
141
|
+
case end_time
|
142
|
+
when Time
|
143
|
+
if exclude_end
|
144
|
+
end_ulid = min(moment: end_time)
|
145
|
+
else
|
146
|
+
end_ulid = max(moment: end_time)
|
147
|
+
end
|
148
|
+
when nil
|
149
|
+
# The end should be max and include end, because nil end means to cover endless ULIDs until the limit
|
150
|
+
end_ulid = max
|
151
|
+
exclude_end = false
|
152
|
+
else
|
153
|
+
raise argument_error_for_range_building(time_range)
|
154
|
+
end
|
155
|
+
|
156
|
+
begin_ulid.freeze
|
157
|
+
end_ulid.freeze
|
158
|
+
|
159
|
+
Range.new(begin_ulid, end_ulid, exclude_end)
|
160
|
+
end
|
161
|
+
|
162
|
+
# @param [Time] time
|
163
|
+
# @return [Time]
|
164
|
+
def self.floor(time)
|
165
|
+
if RUBY_VERSION >= '2.7'
|
166
|
+
time.floor(3)
|
167
|
+
else
|
168
|
+
Time.at(0, milliseconds_from_time(time), :millisecond)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
125
172
|
# @return [Integer]
|
126
173
|
def self.current_milliseconds
|
127
|
-
|
174
|
+
milliseconds_from_time(Time.now)
|
128
175
|
end
|
129
176
|
|
130
177
|
# @param [Time] time
|
131
178
|
# @return [Integer]
|
132
|
-
def self.
|
179
|
+
def self.milliseconds_from_time(time)
|
133
180
|
(time.to_r * 1000).to_i
|
134
181
|
end
|
135
182
|
|
183
|
+
# @param [Time, Integer] moment
|
184
|
+
# @return [Integer]
|
185
|
+
def self.milliseconds_from_moment(moment)
|
186
|
+
moment.kind_of?(Time) ? milliseconds_from_time(moment) : moment.to_int
|
187
|
+
end
|
188
|
+
|
136
189
|
# @return [Integer]
|
137
190
|
def self.reasonable_entropy
|
138
191
|
SecureRandom.random_number(MAX_ENTROPY)
|
139
192
|
end
|
140
193
|
|
194
|
+
# @api private
|
195
|
+
# @deprecated Just exists to compare performance with old implementation. ref: https://github.com/kachick/ruby-ulid/issues/7
|
141
196
|
# @param [String, #to_str] string
|
142
197
|
# @return [ULID]
|
143
198
|
# @raise [ParserError] if the given format is not correct for ULID specs
|
144
199
|
# @raise [OverflowError] if the given value is larger than the ULID limit
|
145
|
-
def self.
|
200
|
+
def self.parse_with_integer_base(string)
|
146
201
|
begin
|
147
202
|
string = string.to_str
|
148
203
|
unless string.size == ENCODED_ID_LENGTH
|
@@ -159,6 +214,67 @@ class ULID
|
|
159
214
|
new milliseconds: milliseconds, entropy: entropy
|
160
215
|
end
|
161
216
|
|
217
|
+
n32_chars = [*'0'..'9', *'A'..'V'].map(&:freeze).freeze
|
218
|
+
raise SetupError, 'obvious bug exists in the mapping algorithm' unless n32_chars.size == 32
|
219
|
+
|
220
|
+
n32_char_by_number = {}
|
221
|
+
n32_chars.each_with_index do |char, index|
|
222
|
+
n32_char_by_number[index] = char
|
223
|
+
end
|
224
|
+
n32_char_by_number.freeze
|
225
|
+
|
226
|
+
# Currently supporting only for `subset for actual use-case`
|
227
|
+
# See below
|
228
|
+
# * https://github.com/ulid/spec/pull/57
|
229
|
+
# * https://github.com/kachick/ruby-ulid/issues/57
|
230
|
+
# * https://github.com/kachick/ruby-ulid/issues/78
|
231
|
+
crockford_base32_mappings = {
|
232
|
+
'J' => 18,
|
233
|
+
'K' => 19,
|
234
|
+
'M' => 20,
|
235
|
+
'N' => 21,
|
236
|
+
'P' => 22,
|
237
|
+
'Q' => 23,
|
238
|
+
'R' => 24,
|
239
|
+
'S' => 25,
|
240
|
+
'T' => 26,
|
241
|
+
'V' => 27,
|
242
|
+
'W' => 28,
|
243
|
+
'X' => 29,
|
244
|
+
'Y' => 30,
|
245
|
+
'Z' => 31
|
246
|
+
}.freeze
|
247
|
+
|
248
|
+
REPLACING_MAP = ENCODING_CHARS.each_with_object({}) do |encoding_char, map|
|
249
|
+
if n = crockford_base32_mappings[encoding_char]
|
250
|
+
char_32 = n32_char_by_number.fetch(n)
|
251
|
+
map[encoding_char] = char_32
|
252
|
+
end
|
253
|
+
end.freeze
|
254
|
+
raise SetupError, 'obvious bug exists in the mapping algorithm' unless REPLACING_MAP.keys == crockford_base32_mappings.keys
|
255
|
+
REPLACING_PATTERN = /[#{REPLACING_MAP.keys.join}]/.freeze
|
256
|
+
|
257
|
+
def self.parse(string)
|
258
|
+
begin
|
259
|
+
string = string.to_str
|
260
|
+
raise "given argument does not match to `#{STRICT_PATTERN.inspect}`" unless STRICT_PATTERN.match?(string)
|
261
|
+
n32encoded = convert_crockford_base32_to_n32(string.upcase)
|
262
|
+
timestamp = n32encoded.slice(0, TIMESTAMP_PART_LENGTH)
|
263
|
+
randomness = n32encoded.slice(TIMESTAMP_PART_LENGTH, RANDOMNESS_PART_LENGTH)
|
264
|
+
milliseconds = timestamp.to_i(32)
|
265
|
+
entropy = randomness.to_i(32)
|
266
|
+
rescue => err
|
267
|
+
raise ParserError, "parsing failure as #{err.inspect} for given #{string.inspect}"
|
268
|
+
end
|
269
|
+
|
270
|
+
new milliseconds: milliseconds, entropy: entropy
|
271
|
+
end
|
272
|
+
|
273
|
+
# @api private
|
274
|
+
private_class_method def self.convert_crockford_base32_to_n32(string)
|
275
|
+
string.gsub(REPLACING_PATTERN, REPLACING_MAP)
|
276
|
+
end
|
277
|
+
|
162
278
|
# @return [Boolean]
|
163
279
|
def self.valid?(string)
|
164
280
|
parse(string)
|
@@ -168,6 +284,7 @@ class ULID
|
|
168
284
|
true
|
169
285
|
end
|
170
286
|
|
287
|
+
# @api private
|
171
288
|
# @param [Integer] integer
|
172
289
|
# @param [Integer] length
|
173
290
|
# @return [Array<Integer>]
|
@@ -179,6 +296,7 @@ class ULID
|
|
179
296
|
digits.reverse!
|
180
297
|
end
|
181
298
|
|
299
|
+
# @api private
|
182
300
|
# @see The logics taken from https://bugs.ruby-lang.org/issues/14401, thanks!
|
183
301
|
# @param [Array<Integer>] reversed_digits
|
184
302
|
# @return [Integer]
|
@@ -191,8 +309,14 @@ class ULID
|
|
191
309
|
num
|
192
310
|
end
|
193
311
|
|
312
|
+
# @return [ArgumentError]
|
313
|
+
private_class_method def self.argument_error_for_range_building(argument)
|
314
|
+
ArgumentError.new "ULID.range takes only `Range[Time]` or `Range[nil]`, given: #{argument.inspect}"
|
315
|
+
end
|
316
|
+
|
194
317
|
attr_reader :milliseconds, :entropy
|
195
318
|
|
319
|
+
# @api private
|
196
320
|
# @param [Integer] milliseconds
|
197
321
|
# @param [Integer] entropy
|
198
322
|
# @return [void]
|
@@ -210,10 +334,9 @@ class ULID
|
|
210
334
|
end
|
211
335
|
|
212
336
|
# @return [String]
|
213
|
-
def
|
337
|
+
def to_s
|
214
338
|
@string ||= Integer::Base.string_for(to_i, ENCODING_CHARS).rjust(ENCODED_ID_LENGTH, '0').upcase.freeze
|
215
339
|
end
|
216
|
-
alias_method :to_s, :to_str
|
217
340
|
|
218
341
|
# @return [Integer]
|
219
342
|
def to_i
|
@@ -228,7 +351,7 @@ class ULID
|
|
228
351
|
|
229
352
|
# @return [String]
|
230
353
|
def inspect
|
231
|
-
@inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{
|
354
|
+
@inspect ||= "ULID(#{to_time.strftime(TIME_FORMAT_IN_INSPECT)}: #{to_s})".freeze
|
232
355
|
end
|
233
356
|
|
234
357
|
# @return [Boolean]
|
@@ -255,7 +378,13 @@ class ULID
|
|
255
378
|
|
256
379
|
# @return [Time]
|
257
380
|
def to_time
|
258
|
-
@time ||=
|
381
|
+
@time ||= begin
|
382
|
+
if RUBY_VERSION >= '2.7'
|
383
|
+
Time.at(0, @milliseconds, :millisecond, in: 'UTC').freeze
|
384
|
+
else
|
385
|
+
Time.at(0, @milliseconds, :millisecond).utc.freeze
|
386
|
+
end
|
387
|
+
end
|
259
388
|
end
|
260
389
|
|
261
390
|
# @return [Array(Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)]
|
@@ -283,11 +412,13 @@ class ULID
|
|
283
412
|
@randomness ||= matchdata[:randomness].freeze
|
284
413
|
end
|
285
414
|
|
415
|
+
# @deprecated This method might be changed in https://github.com/kachick/ruby-ulid/issues/84
|
286
416
|
# @return [Regexp]
|
287
417
|
def pattern
|
288
418
|
@pattern ||= /(?<timestamp>#{timestamp})(?<randomness>#{randomness})/i.freeze
|
289
419
|
end
|
290
420
|
|
421
|
+
# @deprecated This method might be changed in https://github.com/kachick/ruby-ulid/issues/84
|
291
422
|
# @return [Regexp]
|
292
423
|
def strict_pattern
|
293
424
|
@strict_pattern ||= /\A#{pattern.source}\z/i.freeze
|
@@ -308,15 +439,21 @@ class ULID
|
|
308
439
|
@pred ||= self.class.from_integer(pre_int)
|
309
440
|
end
|
310
441
|
|
442
|
+
# @return [String]
|
443
|
+
def to_uuidv4
|
444
|
+
@uuidv4 ||= begin
|
445
|
+
# This code referenced https://github.com/ruby/ruby/blob/121fa24a3451b45c41ac0a661b64e9fc8600e589/lib/securerandom.rb#L221-L241
|
446
|
+
array = octets.pack('C*').unpack('NnnnnN')
|
447
|
+
array[2] = (array[2] & 0x0fff) | 0x4000
|
448
|
+
array[3] = (array[3] & 0x3fff) | 0x8000
|
449
|
+
('%08x-%04x-%04x-%04x-%04x%08x' % array).freeze
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
311
453
|
# @return [self]
|
312
454
|
def freeze
|
313
|
-
#
|
314
|
-
|
315
|
-
octets
|
316
|
-
to_i
|
317
|
-
succ
|
318
|
-
pred
|
319
|
-
strict_pattern
|
455
|
+
# Need to cache before freezing, because frozen objects can't assign instance variables
|
456
|
+
cache_all_instance_variables
|
320
457
|
super
|
321
458
|
end
|
322
459
|
|
@@ -324,7 +461,18 @@ class ULID
|
|
324
461
|
|
325
462
|
# @return [MatchData]
|
326
463
|
def matchdata
|
327
|
-
@matchdata ||= STRICT_PATTERN.match(
|
464
|
+
@matchdata ||= STRICT_PATTERN.match(to_s).freeze
|
465
|
+
end
|
466
|
+
|
467
|
+
# @return [void]
|
468
|
+
def cache_all_instance_variables
|
469
|
+
inspect
|
470
|
+
octets
|
471
|
+
to_i
|
472
|
+
succ
|
473
|
+
pred
|
474
|
+
strict_pattern
|
475
|
+
to_uuidv4
|
328
476
|
end
|
329
477
|
end
|
330
478
|
|
@@ -332,7 +480,9 @@ require_relative 'ulid/version'
|
|
332
480
|
require_relative 'ulid/monotonic_generator'
|
333
481
|
|
334
482
|
class ULID
|
483
|
+
MIN = parse('00000000000000000000000000').freeze
|
484
|
+
MAX = parse('7ZZZZZZZZZZZZZZZZZZZZZZZZZ').freeze
|
335
485
|
MONOTONIC_GENERATOR = MonotonicGenerator.new
|
336
486
|
|
337
|
-
private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN
|
487
|
+
private_constant :ENCODING_CHARS, :TIME_FORMAT_IN_INSPECT, :UUIDV4_PATTERN, :MIN, :MAX, :REPLACING_PATTERN, :REPLACING_MAP
|
338
488
|
end
|
@@ -4,35 +4,37 @@
|
|
4
4
|
|
5
5
|
class ULID
|
6
6
|
class MonotonicGenerator
|
7
|
+
# @api private
|
7
8
|
attr_accessor :latest_milliseconds, :latest_entropy
|
8
9
|
|
9
10
|
def initialize
|
10
11
|
reset
|
11
12
|
end
|
12
13
|
|
13
|
-
# @
|
14
|
+
# @param [Time, Integer] moment
|
14
15
|
# @return [ULID]
|
15
|
-
|
16
|
-
|
17
|
-
|
16
|
+
# @raise [OverflowError] if the entropy part is larger than the ULID limit in same milliseconds
|
17
|
+
# @raise [ArgumentError] if the given moment(milliseconds) is negative number
|
18
|
+
def generate(moment: ULID.current_milliseconds)
|
19
|
+
milliseconds = ULID.milliseconds_from_moment(moment)
|
20
|
+
raise ArgumentError, "milliseconds should not be negative: given: #{milliseconds}" if milliseconds.negative?
|
18
21
|
|
19
|
-
@latest_milliseconds
|
20
|
-
@latest_entropy ||= reasonable_entropy
|
21
|
-
if @latest_milliseconds != milliseconds
|
22
|
+
if @latest_milliseconds < milliseconds
|
22
23
|
@latest_milliseconds = milliseconds
|
23
|
-
@latest_entropy = reasonable_entropy
|
24
|
+
@latest_entropy = ULID.reasonable_entropy
|
24
25
|
else
|
25
26
|
@latest_entropy += 1
|
26
27
|
end
|
27
28
|
|
28
|
-
ULID.new milliseconds:
|
29
|
+
ULID.new milliseconds: @latest_milliseconds, entropy: @latest_entropy
|
29
30
|
end
|
30
31
|
|
31
|
-
# @
|
32
|
+
# @api private
|
33
|
+
# @return [void]
|
32
34
|
def reset
|
33
|
-
@latest_milliseconds =
|
34
|
-
@latest_entropy =
|
35
|
-
|
35
|
+
@latest_milliseconds = 0
|
36
|
+
@latest_entropy = ULID.reasonable_entropy
|
37
|
+
nil
|
36
38
|
end
|
37
39
|
|
38
40
|
# @raise [TypeError] always raises exception and does not freeze self
|
data/lib/ulid/version.rb
CHANGED
data/sig/ulid.rbs
CHANGED
@@ -15,9 +15,16 @@ class ULID
|
|
15
15
|
PATTERN: Regexp
|
16
16
|
STRICT_PATTERN: Regexp
|
17
17
|
UUIDV4_PATTERN: Regexp
|
18
|
+
REPLACING_MAP: Hash[String, String]
|
19
|
+
REPLACING_PATTERN: Regexp
|
18
20
|
MONOTONIC_GENERATOR: MonotonicGenerator
|
21
|
+
MIN: ULID
|
22
|
+
MAX: ULID
|
19
23
|
include Comparable
|
20
24
|
|
25
|
+
# The `moment` is a `Time` or `Intger of the milliseconds`
|
26
|
+
type moment = Time | Integer
|
27
|
+
|
21
28
|
class Error < StandardError
|
22
29
|
end
|
23
30
|
|
@@ -27,16 +34,18 @@ class ULID
|
|
27
34
|
class ParserError < Error
|
28
35
|
end
|
29
36
|
|
37
|
+
class SetupError < ScriptError
|
38
|
+
end
|
39
|
+
|
30
40
|
class MonotonicGenerator
|
31
|
-
attr_accessor latest_milliseconds: Integer
|
32
|
-
attr_accessor latest_entropy: Integer
|
41
|
+
attr_accessor latest_milliseconds: Integer
|
42
|
+
attr_accessor latest_entropy: Integer
|
33
43
|
def initialize: -> void
|
34
|
-
def generate: -> ULID
|
44
|
+
def generate: (?moment: moment) -> ULID
|
35
45
|
def reset: -> void
|
36
46
|
def freeze: -> void
|
37
47
|
end
|
38
48
|
|
39
|
-
type moment = Time | Integer
|
40
49
|
type octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
|
41
50
|
type timestamp_octets = [Integer, Integer, Integer, Integer, Integer, Integer]
|
42
51
|
type randomness_octets = [Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer]
|
@@ -55,12 +64,16 @@ class ULID
|
|
55
64
|
@next: ULID?
|
56
65
|
@pattern: Regexp?
|
57
66
|
@strict_pattern: Regexp?
|
67
|
+
@uuidv4: String?
|
58
68
|
@matchdata: MatchData?
|
59
69
|
|
60
70
|
def self.generate: (?moment: moment, ?entropy: Integer) -> ULID
|
61
71
|
def self.monotonic_generate: -> ULID
|
62
72
|
def self.current_milliseconds: -> Integer
|
63
|
-
def self.
|
73
|
+
def self.milliseconds_from_time: (Time time) -> Integer
|
74
|
+
def self.milliseconds_from_moment: (moment moment) -> Integer
|
75
|
+
def self.range: (Range[Time] | Range[nil] time_range) -> Range[ULID]
|
76
|
+
def self.floor: (Time time) -> Time
|
64
77
|
def self.reasonable_entropy: -> Integer
|
65
78
|
def self.parse: (String string) -> ULID
|
66
79
|
def self.from_uuidv4: (String uuid) -> ULID
|
@@ -72,11 +85,11 @@ class ULID
|
|
72
85
|
| (String string) { (ULID ulid) -> void } -> singleton(ULID)
|
73
86
|
def self.octets_from_integer: (Integer integer, length: Integer) -> Array[Integer]
|
74
87
|
def self.inverse_of_digits: (Array[Integer] reversed_digits) -> Integer
|
88
|
+
def self.convert_crockford_base32_to_n32: (String) -> String
|
75
89
|
attr_reader milliseconds: Integer
|
76
90
|
attr_reader entropy: Integer
|
77
91
|
def initialize: (milliseconds: Integer, entropy: Integer) -> void
|
78
|
-
def
|
79
|
-
alias to_s to_str
|
92
|
+
def to_s: -> String
|
80
93
|
def to_i: -> Integer
|
81
94
|
alias hash to_i
|
82
95
|
def <=>: (ULID other) -> Integer
|
@@ -93,11 +106,14 @@ class ULID
|
|
93
106
|
def octets: -> octets
|
94
107
|
def timestamp_octets: -> timestamp_octets
|
95
108
|
def randomness_octets: -> randomness_octets
|
109
|
+
def to_uuidv4: -> String
|
96
110
|
def next: -> ULID?
|
97
111
|
alias succ next
|
98
112
|
def pred: -> ULID?
|
99
113
|
def freeze: -> self
|
100
114
|
|
101
115
|
private
|
116
|
+
def self.argument_error_for_range_building: (untyped argument) -> ArgumentError
|
102
117
|
def matchdata: -> MatchData
|
118
|
+
def cache_all_instance_variables: -> void
|
103
119
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-ulid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.16
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kenichi Kamiya
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-05-
|
11
|
+
date: 2021-05-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: integer-base
|
@@ -30,6 +30,20 @@ dependencies:
|
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: 0.2.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: rbs
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 1.2.0
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 1.2.0
|
33
47
|
- !ruby/object:Gem::Dependency
|
34
48
|
name: benchmark-ips
|
35
49
|
requirement: !ruby/object:Gem::Requirement
|
@@ -70,10 +84,10 @@ dependencies:
|
|
70
84
|
- - "<"
|
71
85
|
- !ruby/object:Gem::Version
|
72
86
|
version: '2'
|
73
|
-
description:
|
74
|
-
|
75
|
-
|
76
|
-
|
87
|
+
description: |2
|
88
|
+
The ULID(Universally Unique Lexicographically Sortable Identifier) has useful specs for applications (e.g. `Database key`), especially possess all `uniqueness`, `randomness`, `extractable timestamps` and `sortable` features.
|
89
|
+
This gem aims to provide the generator, monotonic generator, parser and handy manipulation features around the ULID.
|
90
|
+
Also providing `ruby/rbs` signature files.
|
77
91
|
email:
|
78
92
|
- kachick1+ruby@gmail.com
|
79
93
|
executables: []
|
@@ -82,7 +96,6 @@ extra_rdoc_files: []
|
|
82
96
|
files:
|
83
97
|
- LICENSE
|
84
98
|
- README.md
|
85
|
-
- Steepfile
|
86
99
|
- lib/ulid.rb
|
87
100
|
- lib/ulid/monotonic_generator.rb
|
88
101
|
- lib/ulid/version.rb
|
@@ -102,7 +115,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
102
115
|
requirements:
|
103
116
|
- - ">="
|
104
117
|
- !ruby/object:Gem::Version
|
105
|
-
version:
|
118
|
+
version: 2.6.0
|
106
119
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
120
|
requirements:
|
108
121
|
- - ">="
|