identifold 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 +7 -0
- data/README.md +10 -0
- data/lib/identifold.rb +165 -0
- metadata +40 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f99add5498f8f16b2d918e04eca8814b5193909b1e7151874ececd4556f23c65
|
|
4
|
+
data.tar.gz: e6a40150899cad674ac403bf60dc0bc1b786e52728d7768f5143e4d8bf11ad7b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b01e97aef84dade2e6f8d4c91fb5790563285d2639250cdd23ab31cc4dc9d0a750940e07e1b23b2e787cb34c89613199f99f91131d9dbc9b3b867828978f245a
|
|
7
|
+
data.tar.gz: 6027d7c7e7ea6b847764e75f88118149eb80cadc4c4b80ed3571eec914d6f7c247cb0f5f41b0792a2d9ca8f08f83d63d6b9633df6760f1ebc6bc9edcd955fbd6
|
data/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Identifold for Ruby
|
|
2
|
+
|
|
3
|
+
The Ruby gem implements the stable Identifold 1.0 MID, PID, and REF wire contract for Ruby 3.2 and later.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
pid = Identifold.public_id_from_machine_id(mid, "order")
|
|
7
|
+
parsed = Identifold.parse_public_id(pid)
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The dependency-free core and JSON adapter are exercised by the language-neutral conformance runner.
|
data/lib/identifold.rb
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
module Identifold
|
|
2
|
+
DATA = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
3
|
+
CHECK = DATA + "*~$=U"
|
|
4
|
+
TYPE_ID = "0123456789abcdefghjkmnpqrstvwxyz"
|
|
5
|
+
|
|
6
|
+
class Error < ArgumentError
|
|
7
|
+
attr_reader :code
|
|
8
|
+
|
|
9
|
+
def initialize(code)
|
|
10
|
+
super(code)
|
|
11
|
+
@code = code
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def parse_machine_id(input)
|
|
18
|
+
value = input.to_s.downcase
|
|
19
|
+
raise Error, "invalid_mid" unless value.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/)
|
|
20
|
+
raise Error, "invalid_uuid_version" unless value[14] == "7"
|
|
21
|
+
raise Error, "invalid_mid" unless "89ab".include?(value[19])
|
|
22
|
+
|
|
23
|
+
value
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def public_id_from_machine_id(machine_id, namespace)
|
|
27
|
+
raise Error, "invalid_public_prefix" unless namespace.match?(/\A[a-z](?:[a-z_]{0,61}[a-z])?\z/)
|
|
28
|
+
|
|
29
|
+
number = parse_machine_id(machine_id).delete("-").to_i(16)
|
|
30
|
+
suffix = Array.new(26, "0")
|
|
31
|
+
25.downto(0) do |index|
|
|
32
|
+
suffix[index] = TYPE_ID[number & 31]
|
|
33
|
+
number >>= 5
|
|
34
|
+
end
|
|
35
|
+
"#{namespace}_#{suffix.join}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse_public_id(value)
|
|
39
|
+
raise Error, "invalid_pid" unless value == value.downcase
|
|
40
|
+
|
|
41
|
+
separator = value.rindex("_")
|
|
42
|
+
raise Error, "invalid_public_prefix" unless separator
|
|
43
|
+
|
|
44
|
+
namespace = value[...separator]
|
|
45
|
+
suffix = value[(separator + 1)..]
|
|
46
|
+
raise Error, "invalid_public_prefix" unless namespace.match?(/\A[a-z](?:[a-z_]{0,61}[a-z])?\z/)
|
|
47
|
+
raise Error, "invalid_pid" unless suffix.length == 26 && suffix[0] <= "7"
|
|
48
|
+
|
|
49
|
+
number = 0
|
|
50
|
+
suffix.each_char do |symbol|
|
|
51
|
+
position = TYPE_ID.index(symbol)
|
|
52
|
+
raise Error, "invalid_pid" unless position
|
|
53
|
+
|
|
54
|
+
number = (number << 5) | position
|
|
55
|
+
end
|
|
56
|
+
hex = number.to_s(16).rjust(32, "0")
|
|
57
|
+
machine_id = [hex[0, 8], hex[8, 4], hex[12, 4], hex[16, 4], hex[20, 12]].join("-")
|
|
58
|
+
begin
|
|
59
|
+
machine_id = parse_machine_id(machine_id)
|
|
60
|
+
rescue Error
|
|
61
|
+
raise Error, "invalid_pid"
|
|
62
|
+
end
|
|
63
|
+
{ value: value, namespace: namespace, machineId: machine_id }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def check_symbol(payload, sequential)
|
|
67
|
+
alphabet = sequential ? "0123456789" : DATA
|
|
68
|
+
base = sequential ? 10 : 32
|
|
69
|
+
remainder = 0
|
|
70
|
+
payload.each_char do |symbol|
|
|
71
|
+
position = alphabet.index(symbol)
|
|
72
|
+
raise Error, "invalid_ref_symbol" unless position
|
|
73
|
+
|
|
74
|
+
remainder = (remainder * base + position) % 37
|
|
75
|
+
end
|
|
76
|
+
CHECK[remainder]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def create_reference_candidate(registry, namespace, random_bytes)
|
|
80
|
+
reference = find_namespace(registry, namespace)["reference"]
|
|
81
|
+
raise Error, "unknown_namespace" unless reference && reference["strategy"] == "random"
|
|
82
|
+
|
|
83
|
+
length = profile_length(reference["profile"])
|
|
84
|
+
raise Error, "invalid_random_source" if random_bytes.length < length
|
|
85
|
+
|
|
86
|
+
payload = random_bytes.first(length).map do |value|
|
|
87
|
+
raise Error, "invalid_random_source" unless value.between?(0, 255)
|
|
88
|
+
|
|
89
|
+
DATA[value % 32]
|
|
90
|
+
end.join
|
|
91
|
+
"#{reference["prefix"]}-#{group(payload)}-#{check_symbol(payload, false)}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def format_sequential_reference(registry, namespace, sequence, scope = "")
|
|
95
|
+
reference = find_namespace(registry, namespace)["reference"]
|
|
96
|
+
raise Error, "unknown_namespace" unless reference && reference["strategy"] == "sequence"
|
|
97
|
+
|
|
98
|
+
width = reference["width"]
|
|
99
|
+
raise Error, "sequence_overflow" unless sequence.match?(/\A\d+\z/) && sequence.length <= width
|
|
100
|
+
|
|
101
|
+
padded = sequence.rjust(width, "0")
|
|
102
|
+
payload = scope + padded
|
|
103
|
+
"#{reference["prefix"]}-#{scope.empty? ? "" : "#{scope}-"}#{padded}-#{check_symbol(payload, true)}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def normalize(value, registry)
|
|
107
|
+
if value.include?("_")
|
|
108
|
+
parsed = parse_public_id(value)
|
|
109
|
+
find_namespace(registry, parsed[:namespace])
|
|
110
|
+
return parsed[:value]
|
|
111
|
+
end
|
|
112
|
+
return parse_machine_id(value) if value.length == 36
|
|
113
|
+
|
|
114
|
+
compact = value.upcase.delete("-")
|
|
115
|
+
definition = registry.find do |candidate|
|
|
116
|
+
reference = candidate["reference"]
|
|
117
|
+
reference && compact.start_with?(reference["prefix"])
|
|
118
|
+
end
|
|
119
|
+
unless definition
|
|
120
|
+
raise Error, value.match?(/\A[A-Za-z]{2,8}/) ? "unknown_namespace" : "invalid_kind"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
reference = definition["reference"]
|
|
124
|
+
body = compact[reference["prefix"].length..]
|
|
125
|
+
raise Error, "invalid_ref" if body.include?("?") || body.include?("_")
|
|
126
|
+
|
|
127
|
+
if reference["strategy"] == "sequence"
|
|
128
|
+
scope_length = reference["scope"] == "calendar-year" ? 4 : 0
|
|
129
|
+
raise Error, "invalid_ref_length" unless body.length == scope_length + reference["width"] + 1
|
|
130
|
+
|
|
131
|
+
payload = body[...-1]
|
|
132
|
+
raise Error, "invalid_checksum" unless check_symbol(payload, true) == body[-1]
|
|
133
|
+
|
|
134
|
+
return format_sequential_reference(
|
|
135
|
+
registry,
|
|
136
|
+
definition["publicPrefix"],
|
|
137
|
+
payload[scope_length..],
|
|
138
|
+
payload[0, scope_length]
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
length = profile_length(reference["profile"])
|
|
143
|
+
raise Error, "invalid_ref_length" unless body.length == length + 1
|
|
144
|
+
|
|
145
|
+
payload = body[0, length].tr("OIL", "011")
|
|
146
|
+
raise Error, "invalid_checksum" unless check_symbol(payload, false) == body[-1]
|
|
147
|
+
|
|
148
|
+
"#{reference["prefix"]}-#{group(payload)}-#{body[-1]}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def find_namespace(registry, namespace)
|
|
152
|
+
registry.find { |definition| definition["publicPrefix"] == namespace } || raise(Error, "unknown_namespace")
|
|
153
|
+
end
|
|
154
|
+
private_class_method :find_namespace
|
|
155
|
+
|
|
156
|
+
def profile_length(profile)
|
|
157
|
+
{ "compact" => 8, "high" => 12 }.fetch(profile, 10)
|
|
158
|
+
end
|
|
159
|
+
private_class_method :profile_length
|
|
160
|
+
|
|
161
|
+
def group(value)
|
|
162
|
+
value.scan(/.{1,4}/).join("-")
|
|
163
|
+
end
|
|
164
|
+
private_class_method :group
|
|
165
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: identifold
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Greyfoundry
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: UUIDv7 MIDs, TypeID-compatible PIDs, and checksummed human references.
|
|
13
|
+
executables: []
|
|
14
|
+
extensions: []
|
|
15
|
+
extra_rdoc_files: []
|
|
16
|
+
files:
|
|
17
|
+
- README.md
|
|
18
|
+
- lib/identifold.rb
|
|
19
|
+
homepage: https://github.com/greyfoundry/identifold
|
|
20
|
+
licenses:
|
|
21
|
+
- Apache-2.0
|
|
22
|
+
metadata: {}
|
|
23
|
+
rdoc_options: []
|
|
24
|
+
require_paths:
|
|
25
|
+
- lib
|
|
26
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
27
|
+
requirements:
|
|
28
|
+
- - ">="
|
|
29
|
+
- !ruby/object:Gem::Version
|
|
30
|
+
version: '3.2'
|
|
31
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
32
|
+
requirements:
|
|
33
|
+
- - ">="
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '0'
|
|
36
|
+
requirements: []
|
|
37
|
+
rubygems_version: 4.0.16
|
|
38
|
+
specification_version: 4
|
|
39
|
+
summary: Identifold 1.0 identifiers for Ruby
|
|
40
|
+
test_files: []
|