fast-uuid 0.1.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 +7 -0
- data/CHANGELOG.md +18 -0
- data/README.md +116 -0
- data/lib/fast/uuid/errors.rb +10 -0
- data/lib/fast/uuid/uuid.rb +375 -0
- data/lib/fast/uuid/version.rb +7 -0
- data/lib/fast/uuid.rb +5 -0
- metadata +48 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0a8b04df8089715f0156dc86feb6b73dd3abebea996b08c213e9b43844bfc9ce
|
|
4
|
+
data.tar.gz: a6e2cae4c744b1b238af21188dab84a6e2830e2108760db5e71c465a789205dd
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a629ab2256ed2043b278d6953b67a96a057888c60a01bed3cd740f9690047be5dcfcec16d0e3f92ec47e91eef051cd24a48f9d3d4d2ee6758fa9c6efe0a05cf4
|
|
7
|
+
data.tar.gz: 4037526e2102bbcf3cfff76b8a2a700ebcfe29ceb85c0793e9fb3b7aac35600ae898c312fa7130c1f6e594e4920e033fef8e2744d18fef79a3c3c5b0d2a932ba
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- Initial release: `Fast::UUID`, one immutable, frozen value class. Generate
|
|
6
|
+
v4 (122 random bits from `SecureRandom`) and v7 (48-bit `unix_ts_ms` plus
|
|
7
|
+
74 fully random bits, no counter and no shared state, thread-safe), with
|
|
8
|
+
`generate` defaulting to v7. Parse and validate every RFC 9562 version and
|
|
9
|
+
variant — validity is the RFC 4 grammar and nothing more, case-insensitive,
|
|
10
|
+
Nil and Max included — with `parse`, `parse?` and `valid?`. `Fast::UUID.new`
|
|
11
|
+
builds from a 16-byte binary String.
|
|
12
|
+
- Timestamps: v1 and v6 read their 60-bit 100-ns Gregorian timestamp, v7 its
|
|
13
|
+
`unix_ts_ms`; every other version reports nil. Comparison is opaque
|
|
14
|
+
unsigned 128-bit byte order, so v6 and v7 sort chronologically; v1 does
|
|
15
|
+
not, its `time_low` sits first, and the README carries the caveat.
|
|
16
|
+
- `to_s`, `to_str`, `bytes`, `hex`, `urn`, `nil_uuid?`, `max_uuid?`,
|
|
17
|
+
`version`, `variant`, and `==`/`eql?`/`hash` over the 128-bit value alone.
|
|
18
|
+
Verified in-suite against all six RFC 9562 Appendix A test vectors.
|
data/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# fast-uuid
|
|
2
|
+
|
|
3
|
+
> **Status:** _Alpha, pre-1.0_ The API surface may still move between v0.x and v1.0.
|
|
4
|
+
|
|
5
|
+
A UUID library for Ruby that is fast, complete, and RFC 9562-compliant.
|
|
6
|
+
|
|
7
|
+
Fast::UUID provides drop-in, api-compatible replacements for `SecureRandom`'s `.uuid_v4` and `.uuid_v7` that are as fast (within 2%) or faster while allocating half as many objects or fewer while using the same source of entropy.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# before
|
|
11
|
+
SecureRandom.uuid_v4 # => "cdbb6d1f-…"
|
|
12
|
+
SecureRandom.uuid_v7 # => "01a12861-…"
|
|
13
|
+
|
|
14
|
+
# after — the same calls, the same entropy, frozen, half the objects or fewer
|
|
15
|
+
Fast::UUID.uuid_v4 # => "e74ce176-…"
|
|
16
|
+
Fast::UUID.uuid_v7 # => "01a12861-…"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Fast::UUID can also return immutable `Fast::UUID` objects that are directly `Comparable`. These also allocate fewer objects than `SecureRandom`'s `.uuid_v4` and `.uuid_v7`.
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
a = Fast::UUID.v7(time: Time.utc(2026, 10, 10))
|
|
23
|
+
b = Fast::UUID.v7(time: Time.utc(2026, 10, 11))
|
|
24
|
+
a < b # => true — v7 byte order is time order
|
|
25
|
+
[a, b].max.timestamp # => 2026-10-11 00:00:00 UTC
|
|
26
|
+
Fast::UUID.parse(a.to_s) == a # => true — identity is the 128-bit value alone
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Fast::UUID's goal is to cover every UUID use-case a project could have in a way that is correct and of course, fast.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Add to your Gemfile:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
gem "fast-uuid"
|
|
37
|
+
```
|
|
38
|
+
and `require "fast/uuid"`.
|
|
39
|
+
|
|
40
|
+
Fast::UUID has no dependencies and is implemented in pure Ruby.
|
|
41
|
+
|
|
42
|
+
## Quickstart
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
require "fast/uuid"
|
|
46
|
+
|
|
47
|
+
uuid = Fast::UUID.generate # v7, the RFC's recommended default
|
|
48
|
+
Fast::UUID.uuid_v4 # => "b4c7b0f1-…", or uuid_v1 - uuid_v7. Returns a String just like SecureRandom
|
|
49
|
+
Fast::UUID.parse(uuid.to_s) == uuid # => true, from any conforming String, any case
|
|
50
|
+
Fast::UUID.v4.version # => 4
|
|
51
|
+
Fast::UUID.v7.timestamp # => a UTC Time, the 48-bit unix_ts_ms
|
|
52
|
+
Fast::UUID.v5(Fast::UUID::NAMESPACE_DNS, "www.example.com").to_s
|
|
53
|
+
# => "2ed6657d-e927-568b-95e1-2665a8aea6a2", same input, same UUID
|
|
54
|
+
|
|
55
|
+
sorted = [Fast::UUID.v7, Fast::UUID.v7].sort # v7 sorts chronologically, RFC 6.11
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`Fast::UUID.parse` raises `Fast::UUID::ParseError` for anything the RFC
|
|
59
|
+
4 grammar rejects and `TypeError` for an object with no `to_s`; `parse?`
|
|
60
|
+
and `valid?` are the non-raising forms. Every version and variant
|
|
61
|
+
parses, Nil and Max included.
|
|
62
|
+
|
|
63
|
+
One caveat the RFC does not decide for you: opaque byte order makes v6 and
|
|
64
|
+
v7 sort chronologically, but **v1 does not**. The
|
|
65
|
+
[tutorial](docs/tutorials/generate-parse-and-sort-uuids.md) walks through the whole
|
|
66
|
+
story: generate, parse, introspect, sort, and a request-ID module at the end
|
|
67
|
+
of it.
|
|
68
|
+
|
|
69
|
+
## Performance
|
|
70
|
+
|
|
71
|
+
Measured on the six operations the gem and its references share — generation
|
|
72
|
+
against `securerandom`, parsing against `uuidtools` (Ruby 4.0.7; conditions,
|
|
73
|
+
the allocation columns and how to reproduce on the
|
|
74
|
+
[benchmarks page](docs/explanation/benchmarks.md)):
|
|
75
|
+
|
|
76
|
+
- Generation is **ahead of the stdlib String for v4, within 10% for v7** —
|
|
77
|
+
0.65M vs 0.64M i/s for v4, 0.58M vs 0.65M for v7, at half or fewer of the
|
|
78
|
+
objects — and the String forms win outright: `uuid_v4` 0.72M vs 0.64M with a
|
|
79
|
+
quarter of the objects, `uuid_v7` 0.64M vs 0.65M — parity — with two fifths.
|
|
80
|
+
- Parsing is **2.7x faster than uuidtools with 4.5x fewer objects** — 1.14M vs
|
|
81
|
+
0.42M i/s, 6 objects per call against 27 — because it is a grammar check and
|
|
82
|
+
one `pack`, not a decoded field graph.
|
|
83
|
+
|
|
84
|
+
## Documentation
|
|
85
|
+
|
|
86
|
+
### Tutorials
|
|
87
|
+
|
|
88
|
+
- [Tutorial: generate, parse and sort UUIDs](docs/tutorials/generate-parse-and-sort-uuids.md) — generate, parse, introspect and sort, ending in a sortable request ID.
|
|
89
|
+
|
|
90
|
+
### How-to guides
|
|
91
|
+
|
|
92
|
+
- [How to choose a UUID version](docs/how-to/choose-a-uuid-version.md) — v7 by default, v6 for a v1 fleet, v5 for name-derived determinism, v4 for randomness, v1 for legacy systems.
|
|
93
|
+
- [How to generate deterministic UUIDs](docs/how-to/generate-deterministic-uuids.md) — pin a v7 to an exact millisecond with `v7(time:)`, pin a v1 or v6 to the 100-ns tick, or derive a stable v5 from a namespace and name.
|
|
94
|
+
- [How to validate untrusted input](docs/how-to/validate-untrusted-input.md) — what `parse?` and `valid?` return, and what `parse` raises.
|
|
95
|
+
- [How to sort UUIDs](docs/how-to/sort-uuids.md) — `<=>` byte order, and the v1 caveat.
|
|
96
|
+
- [How to store a UUID as 16 bytes](docs/how-to/store-a-uuid-as-16-bytes.md) — `bytes` out, `new(bytes)` back.
|
|
97
|
+
|
|
98
|
+
### Reference
|
|
99
|
+
|
|
100
|
+
- [Reference: Fast::UUID](docs/reference/uuid.md) — every class method, instance method, constant and error.
|
|
101
|
+
|
|
102
|
+
### Explanation
|
|
103
|
+
|
|
104
|
+
- [What a UUID is, and what its versions are](docs/explanation/uuid-versions.md) — embedded time, natural sorting, name-derived determinism, and randomness as one option among five.
|
|
105
|
+
- [Why validity is the grammar and nothing more](docs/explanation/validity.md) — why no version or variant gate: the RFC defines none, and Nil and Max sit outside the 10xx variant by design.
|
|
106
|
+
- [In what order UUIDs sort](docs/explanation/comparison-order.md) — opaque bytes per RFC §6.11, v6/v7 chronological, v1 anti-chronological.
|
|
107
|
+
- [Benchmarks](docs/explanation/benchmarks.md) — generation against securerandom, parsing against uuidtools, and how to reproduce them.
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
bundle exec sus
|
|
113
|
+
bundle exec rubocop
|
|
114
|
+
bundle exec ruby benchmark/uuid.rb # full run, about a minute
|
|
115
|
+
BENCH_QUICK=1 bundle exec ruby benchmark/uuid.rb # quick run, seconds
|
|
116
|
+
```
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module Fast
|
|
7
|
+
# An immutable, frozen RFC 9562 UUID value: generate v4/v7, time-based
|
|
8
|
+
# v1/v6 and name-based v3/v5, parse and validate every version and
|
|
9
|
+
# variant, compare as an opaque 128-bit value.
|
|
10
|
+
class UUID
|
|
11
|
+
include Comparable
|
|
12
|
+
|
|
13
|
+
# The canonical form, 4hexOctet-2hexOctet-2hexOctet-2hexOctet-6hexOctet,
|
|
14
|
+
# case-insensitive. Validity is this grammar and nothing more: the RFC
|
|
15
|
+
# defines no version or variant gate, and Nil and Max sit outside the
|
|
16
|
+
# 10xx variant by design.
|
|
17
|
+
CANONICAL = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
|
18
|
+
|
|
19
|
+
# 100-ns ticks between the Gregorian (1582-10-15) and Unix epoch baselines.
|
|
20
|
+
GREGORIAN_OFFSET = 122_192_928_000_000_000
|
|
21
|
+
|
|
22
|
+
# 128 random bits from the CSPRNG, version and variant stamped into
|
|
23
|
+
# place: 122 stay random, version 4 fills one nibble and the 10xx
|
|
24
|
+
# variant the top two bits.
|
|
25
|
+
def self.v4
|
|
26
|
+
new(v4_bytes)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The same 128 random bits as .v4, rendered straight to the canonical
|
|
30
|
+
# form — drop-in for SecureRandom.uuid_v4, the value object skipped.
|
|
31
|
+
def self.uuid_v4
|
|
32
|
+
canonical(v4_bytes)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The time is truncated to whole milliseconds for the 48-bit unix_ts_ms;
|
|
36
|
+
# rand_a and rand_b stay fully random, no counter and no shared state.
|
|
37
|
+
def self.v7(time: Time.now)
|
|
38
|
+
new(v7_bytes(time: time))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The same 48-bit unix_ts_ms and random tail as .v7, rendered straight
|
|
42
|
+
# to the canonical form — drop-in for SecureRandom.uuid_v7.
|
|
43
|
+
def self.uuid_v7(time: Time.now)
|
|
44
|
+
canonical(v7_bytes(time: time))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The 60-bit 100-ns Gregorian timestamp both time-based versions
|
|
48
|
+
# encode, masked to the field the way v7 masks unix_ts_ms.
|
|
49
|
+
def self.gregorian_ticks(time)
|
|
50
|
+
((time.to_i * 10_000_000) + (time.nsec / 100) + GREGORIAN_OFFSET) & 0xFFFFFFFFFFFFFFF
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The node: nil draws a fresh CSPRNG value with the multicast bit set
|
|
54
|
+
# (RFC §6.10), an Integer-like input masks to 48 bits, a string-like
|
|
55
|
+
# input unpacks from its 6 binary bytes — the size is the requirement,
|
|
56
|
+
# not the class — and a Symbol goes by its #name, the frozen String it
|
|
57
|
+
# already carries. The 48 bits travel as hex — a Q> would land past the
|
|
58
|
+
# immediate Integers three times in four and cost an object when it
|
|
59
|
+
# does.
|
|
60
|
+
def self.node_id(node)
|
|
61
|
+
case node
|
|
62
|
+
when nil
|
|
63
|
+
SecureRandom.bytes(6).unpack1("H12").to_i(16) | (1 << 40)
|
|
64
|
+
when Integer
|
|
65
|
+
node & 0xFFFFFFFFFFFF
|
|
66
|
+
when String
|
|
67
|
+
raise TypeError, "a 6-byte node String is required (got #{node.bytesize} bytes)" unless node.bytesize == 6
|
|
68
|
+
|
|
69
|
+
node.b.unpack1("H12").to_i(16)
|
|
70
|
+
when Symbol
|
|
71
|
+
node_id(node.name)
|
|
72
|
+
else
|
|
73
|
+
return node.to_int & 0xFFFFFFFFFFFF if defined?(node.to_int)
|
|
74
|
+
return node_id(node.to_s) if defined?(node.to_s)
|
|
75
|
+
|
|
76
|
+
got = defined?(node.class) ? node.class : "a classless object"
|
|
77
|
+
raise TypeError, "an Integer-like or 6-byte string-like node is required (got #{got})"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The low word v1 and v6 share: the 14-bit clock_seq over the 48-bit
|
|
82
|
+
# node, the variant's top two bits stamped in place after the pack. A
|
|
83
|
+
# nil clock_sequence draws a fresh CSPRNG value, as v4 draws its bits;
|
|
84
|
+
# an Integer-like one masks to 14 bits.
|
|
85
|
+
def self.sequence_low(node, clock_sequence)
|
|
86
|
+
clock_sequence = case clock_sequence
|
|
87
|
+
when Integer then clock_sequence
|
|
88
|
+
when nil then SecureRandom.bytes(2).unpack1("n") & 0x3FFF
|
|
89
|
+
else
|
|
90
|
+
unless defined?(clock_sequence.to_int)
|
|
91
|
+
got = defined?(clock_sequence.class) ? clock_sequence.class : "a classless object"
|
|
92
|
+
raise TypeError, "an Integer-like clock_sequence is required (got #{got})"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
clock_sequence.to_int
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
((clock_sequence & 0x3FFF) << 48) | node_id(node)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The 60-bit Gregorian timestamp split least-significant-first:
|
|
102
|
+
# time_low, then time_mid, then time_hi under the version nibble 1.
|
|
103
|
+
# Where possible, prefer v7 (RFC §5.7): v1's least significant field
|
|
104
|
+
# sits first, so it does not sort chronologically.
|
|
105
|
+
def self.v1(time: Time.now, node: nil, clock_sequence: nil)
|
|
106
|
+
new(v1_bytes(time: time, node: node, clock_sequence: clock_sequence))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The same 60-bit timestamp as .v1, rendered straight to the canonical
|
|
110
|
+
# form with the same node and clock sequence handling.
|
|
111
|
+
def self.uuid_v1(time: Time.now, node: nil, clock_sequence: nil)
|
|
112
|
+
canonical(v1_bytes(time: time, node: node, clock_sequence: clock_sequence))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# v1's timestamp reordered most-significant-first (RFC §5.6, the v1
|
|
116
|
+
# migration version): time_high, then time_mid, then time_low under the
|
|
117
|
+
# version nibble 6. Same node, clock sequence and variant as v1, and it
|
|
118
|
+
# sorts chronologically where v1 does not.
|
|
119
|
+
def self.v6(time: Time.now, node: nil, clock_sequence: nil)
|
|
120
|
+
new(v6_bytes(time: time, node: node, clock_sequence: clock_sequence))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# The same reordered timestamp as .v6, rendered straight to the
|
|
124
|
+
# canonical form.
|
|
125
|
+
def self.uuid_v6(time: Time.now, node: nil, clock_sequence: nil)
|
|
126
|
+
canonical(v6_bytes(time: time, node: node, clock_sequence: clock_sequence))
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# MD5 over the namespace's 16 bytes followed by the name's, version and
|
|
130
|
+
# variant stamped into the whole 128-bit digest. Where possible, prefer
|
|
131
|
+
# v5.
|
|
132
|
+
def self.v3(namespace, name)
|
|
133
|
+
new(v3_bytes(namespace, name))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The same digest as .v3, rendered straight to the canonical form.
|
|
137
|
+
def self.uuid_v3(namespace, name)
|
|
138
|
+
canonical(v3_bytes(namespace, name))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# SHA-1 over the same input as v3, the leftmost 128 bits of its 160-bit
|
|
142
|
+
# digest (the rightmost 32 discarded), version and variant stamped in
|
|
143
|
+
# place.
|
|
144
|
+
def self.v5(namespace, name)
|
|
145
|
+
new(v5_bytes(namespace, name))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# The same digest as .v5, rendered straight to the canonical form.
|
|
149
|
+
def self.uuid_v5(namespace, name)
|
|
150
|
+
canonical(v5_bytes(namespace, name))
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# The RFC's recommended default: v7.
|
|
154
|
+
def self.generate
|
|
155
|
+
v7
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# The RFC's recommended default as a String: the v7 canonical form.
|
|
159
|
+
def self.uuid
|
|
160
|
+
uuid_v7
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# The input of both name-based versions: the namespace's 16 bytes
|
|
164
|
+
# followed by the name's. A UUID namespace is itself and anything else
|
|
165
|
+
# string-like parses as one; the name is the String form it carries —
|
|
166
|
+
# a name has no grammar, every String is one.
|
|
167
|
+
def self.digest_input(namespace, name)
|
|
168
|
+
name = implicit_string(name)
|
|
169
|
+
namespace = case namespace
|
|
170
|
+
when UUID then namespace
|
|
171
|
+
else parse(namespace)
|
|
172
|
+
end
|
|
173
|
+
namespace.bytes + name.b
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private_class_method :digest_input
|
|
177
|
+
|
|
178
|
+
# The 16 raw bytes each version builds, version and variant nibbles
|
|
179
|
+
# stamped in place, shared by the object constructors and their uuid_*
|
|
180
|
+
# String twins.
|
|
181
|
+
def self.v4_bytes
|
|
182
|
+
stamp(SecureRandom.bytes(16), 4)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def self.v7_bytes(time:)
|
|
186
|
+
bytes = SecureRandom.bytes(16)
|
|
187
|
+
milliseconds = (time.to_i * 1000) + (time.nsec / 1_000_000)
|
|
188
|
+
milliseconds &= 0xFFFFFFFFFFFF
|
|
189
|
+
# The masked unix_ts_ms rides in the middle six bytes of an 8-byte
|
|
190
|
+
# word, leaving bytes 6-7 to the random rand_a they already hold.
|
|
191
|
+
bytes.bytesplice(0, 6, [milliseconds].pack("Q>"), 2, 6)
|
|
192
|
+
stamp(bytes, 7)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def self.v1_bytes(time:, node:, clock_sequence:)
|
|
196
|
+
ticks = gregorian_ticks(time)
|
|
197
|
+
bytes = [((ticks & 0xFFFFFFFF) << 32) | (((ticks >> 32) & 0xFFFF) << 16) | (ticks >> 48),
|
|
198
|
+
sequence_low(node, clock_sequence)].pack("Q> Q>")
|
|
199
|
+
stamp(bytes, 1)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def self.v6_bytes(time:, node:, clock_sequence:)
|
|
203
|
+
ticks = gregorian_ticks(time)
|
|
204
|
+
bytes = [((ticks >> 28) << 32) | (((ticks >> 12) & 0xFFFF) << 16) | (ticks & 0xFFF),
|
|
205
|
+
sequence_low(node, clock_sequence)].pack("Q> Q>")
|
|
206
|
+
stamp(bytes, 6)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def self.v3_bytes(namespace, name)
|
|
210
|
+
stamp(Digest::MD5.digest(digest_input(namespace, name)), 3)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def self.v5_bytes(namespace, name)
|
|
214
|
+
stamp(Digest::SHA1.digest(digest_input(namespace, name)).byteslice(0, 16), 5)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# The version nibble and the 10xx variant, masked into the finished 16
|
|
218
|
+
# bytes in place — setbyte arithmetic, zero objects.
|
|
219
|
+
def self.stamp(bytes, version)
|
|
220
|
+
bytes.setbyte(6, (bytes.getbyte(6) & 0x0F) | (version << 4))
|
|
221
|
+
bytes.setbyte(8, (bytes.getbyte(8) & 0x3F) | 0x80)
|
|
222
|
+
bytes
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# The canonical dashed form of 16 stamped bytes: hexencode's lowercase
|
|
226
|
+
# hex, the hyphens inserted in place, frozen like #to_s.
|
|
227
|
+
def self.canonical(bytes)
|
|
228
|
+
Digest.hexencode(bytes).insert(8, "-").insert(13, "-").insert(18, "-").insert(23, "-").freeze
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
private_class_method :v4_bytes, :v7_bytes, :v1_bytes, :v6_bytes, :v3_bytes, :v5_bytes,
|
|
232
|
+
:stamp, :canonical
|
|
233
|
+
|
|
234
|
+
# The String a parse or valid? input carries: a String is itself, a
|
|
235
|
+
# Symbol's frozen #name is already the String it carries — no #to_s
|
|
236
|
+
# copy — anything else responding to #to_s converts, anything else has
|
|
237
|
+
# no string form. A BasicObject descendant has no #is_a?, #class or
|
|
238
|
+
# #respond_to? either, so the probe is defined? — and the TypeError
|
|
239
|
+
# cannot name the class.
|
|
240
|
+
def self.implicit_string(str)
|
|
241
|
+
raise TypeError, "no implicit conversion into Fast::UUID" unless defined?(str.to_s)
|
|
242
|
+
|
|
243
|
+
str = str.name if str.is_a?(Symbol)
|
|
244
|
+
str = str.to_s unless str.is_a?(String)
|
|
245
|
+
str
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
private_class_method :implicit_string
|
|
249
|
+
|
|
250
|
+
def self.parse(str)
|
|
251
|
+
str = implicit_string(str)
|
|
252
|
+
raise ParseError, "not a valid UUID: #{str}" unless CANONICAL.match?(str)
|
|
253
|
+
|
|
254
|
+
new([str.delete("-")].pack("H*"))
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def self.parse?(str)
|
|
258
|
+
parse(str)
|
|
259
|
+
rescue TypeError, ParseError
|
|
260
|
+
nil
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def self.valid?(str)
|
|
264
|
+
CANONICAL.match?(implicit_string(str))
|
|
265
|
+
rescue TypeError
|
|
266
|
+
false
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# From a 16-byte binary String, or anything carrying one via #to_str,
|
|
270
|
+
# the implicit String protocol; an encoding tag on the input is
|
|
271
|
+
# normalized to binary, every other ill shape raises ArgumentError.
|
|
272
|
+
# The probe is defined?, as implicit_string's — a BasicObject
|
|
273
|
+
# descendant has no #is_a? and must ArgumentError, not NoMethodError.
|
|
274
|
+
def initialize(bytes)
|
|
275
|
+
bytes = bytes.to_str if defined?(bytes.to_str) && !bytes.is_a?(String)
|
|
276
|
+
|
|
277
|
+
case bytes
|
|
278
|
+
when String
|
|
279
|
+
unless bytes.bytesize == 16
|
|
280
|
+
raise ArgumentError, "a 16-byte binary String is required (got #{bytes.bytesize} bytes)"
|
|
281
|
+
end
|
|
282
|
+
else
|
|
283
|
+
got = defined?(bytes.class) ? bytes.class : "a classless object"
|
|
284
|
+
raise ArgumentError, "a 16-byte binary String is required (got #{got})"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
@bytes = bytes.b.freeze
|
|
288
|
+
@string = Digest.hexencode(@bytes).insert(8, "-").insert(13, "-").insert(18, "-").insert(23, "-").freeze
|
|
289
|
+
freeze
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def version
|
|
293
|
+
@bytes.getbyte(6) >> 4
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def variant
|
|
297
|
+
case @bytes.getbyte(8) >> 5
|
|
298
|
+
when 0b100, 0b101 then :rfc9562
|
|
299
|
+
when 0b110 then :microsoft
|
|
300
|
+
when 0b111 then :future
|
|
301
|
+
else :ncs
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def timestamp
|
|
306
|
+
case version
|
|
307
|
+
when 1
|
|
308
|
+
# time_low, time_mid, then time_hi_and_version on the wire.
|
|
309
|
+
gregorian(((@bytes[6, 2].unpack1("n") & 0x0FFF) << 48) | (@bytes[4, 2].unpack1("n") << 32) |
|
|
310
|
+
@bytes[0, 4].unpack1("N"))
|
|
311
|
+
when 6
|
|
312
|
+
# time_high, time_mid, time_low in descending order of significance.
|
|
313
|
+
gregorian((@bytes[0, 4].unpack1("N") << 28) | (@bytes[4, 2].unpack1("n") << 12) |
|
|
314
|
+
(@bytes[6, 2].unpack1("n") & 0x0FFF))
|
|
315
|
+
when 7
|
|
316
|
+
milliseconds = @bytes[0, 8].unpack1("Q>") >> 16
|
|
317
|
+
Time.at(milliseconds / 1000, (milliseconds % 1000) * 1_000_000, :nanosecond).utc
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
attr_reader :bytes
|
|
322
|
+
|
|
323
|
+
def to_s
|
|
324
|
+
@string
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
alias to_str to_s
|
|
328
|
+
|
|
329
|
+
def hex
|
|
330
|
+
Digest.hexencode(@bytes)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def urn
|
|
334
|
+
"urn:uuid:#{@string}"
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def nil_uuid?
|
|
338
|
+
self == NIL
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def max_uuid?
|
|
342
|
+
self == MAX
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def ==(other)
|
|
346
|
+
other.is_a?(UUID) && other.bytes == bytes
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
alias eql? ==
|
|
350
|
+
|
|
351
|
+
def hash
|
|
352
|
+
bytes.hash
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Opaque unsigned 128-bit byte order: v6 and v7 sort chronologically as
|
|
356
|
+
# raw bytes; v1 does not, its time_low sits first.
|
|
357
|
+
def <=>(other)
|
|
358
|
+
bytes <=> other.bytes if other.is_a?(UUID)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
private
|
|
362
|
+
|
|
363
|
+
def gregorian(ticks)
|
|
364
|
+
unix_ticks = ticks - GREGORIAN_OFFSET
|
|
365
|
+
Time.at(unix_ticks / 10_000_000, (unix_ticks % 10_000_000) * 100, :nanosecond).utc
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
Fast::UUID::NIL = Fast::UUID.new(("\0" * 16).b)
|
|
371
|
+
Fast::UUID::MAX = Fast::UUID.new(("\xFF" * 16).b)
|
|
372
|
+
Fast::UUID::NAMESPACE_DNS = Fast::UUID.parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
|
|
373
|
+
Fast::UUID::NAMESPACE_URL = Fast::UUID.parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")
|
|
374
|
+
Fast::UUID::NAMESPACE_OID = Fast::UUID.parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")
|
|
375
|
+
Fast::UUID::NAMESPACE_X500 = Fast::UUID.parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")
|
data/lib/fast/uuid.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fast-uuid
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Eric Jacobs
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
executables: []
|
|
13
|
+
extensions: []
|
|
14
|
+
extra_rdoc_files: []
|
|
15
|
+
files:
|
|
16
|
+
- CHANGELOG.md
|
|
17
|
+
- README.md
|
|
18
|
+
- lib/fast/uuid.rb
|
|
19
|
+
- lib/fast/uuid/errors.rb
|
|
20
|
+
- lib/fast/uuid/uuid.rb
|
|
21
|
+
- lib/fast/uuid/version.rb
|
|
22
|
+
homepage: https://github.com/jetpks/fast-uuid
|
|
23
|
+
licenses:
|
|
24
|
+
- MIT
|
|
25
|
+
metadata:
|
|
26
|
+
homepage_uri: https://github.com/jetpks/fast-uuid
|
|
27
|
+
source_code_uri: https://github.com/jetpks/fast-uuid
|
|
28
|
+
changelog_uri: https://github.com/jetpks/fast-uuid/blob/main/CHANGELOG.md
|
|
29
|
+
rubygems_mfa_required: 'true'
|
|
30
|
+
rdoc_options: []
|
|
31
|
+
require_paths:
|
|
32
|
+
- lib
|
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
34
|
+
requirements:
|
|
35
|
+
- - ">="
|
|
36
|
+
- !ruby/object:Gem::Version
|
|
37
|
+
version: '3.3'
|
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '0'
|
|
43
|
+
requirements: []
|
|
44
|
+
rubygems_version: 4.0.20
|
|
45
|
+
specification_version: 4
|
|
46
|
+
summary: 'RFC 9562 UUIDs for Ruby: generate v4/v7, parse every version, compare, no
|
|
47
|
+
dependencies.'
|
|
48
|
+
test_files: []
|