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.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +10 -0
  3. data/lib/identifold.rb +165 -0
  4. 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: []