ashid 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4202347d4a935d5d28ecb9b77c9ea91e7ce374b66ec12e70ee24ee031d41f8e5
4
+ data.tar.gz: f5fc04121314a8fdbf012ad06a6a78ad6d103dffbdf12da716ca98e107b290f7
5
+ SHA512:
6
+ metadata.gz: 98959c0286172b5705b5f27732267630097697bad82edc41a381f6b2a73ff6a35694596d8265c975b257d2d64fd3feebd92eefd54e325ece2d570aa8054f77c4
7
+ data.tar.gz: 8a1e40a69897006d9be2a1c14d39b452f38f02d7b70ace342c74d26ca39779ea38021f026815f3e0a6bbe2f0faf638efcee76e8435cc29a150f933da62f8cdd2
data/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-05-06
11
+
12
+ ### Added
13
+ - Initial release of `ashid` Ruby port.
14
+ - `Ashid.generate(prefix, time:, random:)` — time-sortable IDs with optional Stripe-style prefix.
15
+ - `Ashid.generate4(prefix, random1:, random2:)` — UUIDv4-equivalent random-only IDs.
16
+ - `Ashid.parse`, `.prefix`, `.timestamp`, `.time`, `.random`, `.random_bytes` — inspection helpers.
17
+ - `Ashid.valid?` — non-raising predicate.
18
+ - `Ashid.normalize` — canonicalize lookalikes and casing.
19
+ - `Ashid::Encoder` — Crockford Base32 encoder/decoder + CSPRNG.
20
+ - Cross-language parity test against the TypeScript reference implementation.
21
+ - Custom error hierarchy: `Ashid::Error`, `Ashid::InvalidIdError`, `Ashid::InvalidEncodingError`.
22
+
23
+ [Unreleased]: https://github.com/dankozlowski/ashid-ruby/compare/v0.1.0...HEAD
24
+ [0.1.0]: https://github.com/dankozlowski/ashid-ruby/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dan Kozlowski (Ruby port)
4
+ Copyright (c) 2024 Dathan Guiley / Wilde Agency (original ashid library)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # ashid
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/ashid.svg)](https://rubygems.org/gems/ashid)
4
+ [![Test](https://github.com/dankozlowski/ashid-ruby/actions/workflows/test.yml/badge.svg)](https://github.com/dankozlowski/ashid-ruby/actions/workflows/test.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.txt)
6
+
7
+ **Time-sortable unique identifiers with type prefixes.** Ruby port of the [ashid library](https://github.com/wildeagency/ashid), wire-compatible with the TypeScript and Kotlin implementations.
8
+
9
+ ```ruby
10
+ Ashid.generate("user") # => "user_1je3kvrg000007fn17wx6b"
11
+ Ashid.generate("tx") # => "tx_1je3kvrg1000075n93qdpk"
12
+ Ashid.generate # => "1je3kvrg000007fn17wx6b"
13
+ ```
14
+
15
+ ## Why ashid?
16
+
17
+ UUIDs are opaque. When you see `550e8400-e29b-41d4-a716-446655440000` in a log, you have no idea what it represents.
18
+
19
+ ashid generates IDs that tell you what they are:
20
+
21
+ ```
22
+ user_1je3kvrg000007fn17wx6b ← Obviously a user
23
+ tx_1je3kvrg1000075n93qdpk ← Obviously a transaction
24
+ asset_1je3kvrg200005j6x7eygm ← Obviously an asset
25
+ ```
26
+
27
+ ### Features
28
+
29
+ - **Type prefixes** — Self-documenting IDs, like Stripe (`sk_`, `pi_`, `cus_`).
30
+ - **Time-sortable** — Lexicographic sort = chronological sort.
31
+ - **Crockford Base32** — Case-insensitive, `I/L→1`, `O→0` (no more "is that a zero or an O?").
32
+ - **Double-click selectable** — No hyphens or special characters.
33
+ - **URL-safe** — No encoding required.
34
+ - **Cross-language** — Round-trips with the TypeScript and Kotlin libraries.
35
+ - **Zero runtime dependencies** — Just stdlib `SecureRandom`.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ gem install ashid
41
+ ```
42
+
43
+ Or in a Gemfile:
44
+
45
+ ```ruby
46
+ gem "ashid"
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ ```ruby
52
+ require "ashid"
53
+
54
+ Ashid.generate # => "1je3kvrg000007fn17wx6b"
55
+ Ashid.generate("user") # => "user_1je3kvrg000007fn17wx6b"
56
+ Ashid.generate4("tok") # => "tok_14d2pf2dbsqqg0zvebn63pags1" (random-only, like UUIDv4)
57
+
58
+ id = Ashid.generate("user")
59
+ Ashid.parse(id) # => {prefix: "user_", encoded_timestamp: "...", encoded_random: "..."}
60
+ Ashid.timestamp(id) # => 1733140800000
61
+ Ashid.time(id) # => 2024-12-02 12:00:00 UTC
62
+ Ashid.valid?(id) # => true
63
+ ```
64
+
65
+ ## API Reference
66
+
67
+ ### `Ashid.generate(prefix = nil, time:, random:)`
68
+
69
+ Generate a time-sortable ID, optionally with a prefix.
70
+
71
+ ```ruby
72
+ Ashid.generate # => "1je3kvrg000007fn17wx6b"
73
+ Ashid.generate("user") # => "user_1je3kvrg000007fn17wx6b"
74
+ Ashid.generate("user", time: 1_733_140_800_000, random: 8_234_567_890_123)
75
+ ```
76
+
77
+ - `prefix` (positional, optional): alphanumeric chars are kept and lowercased; everything else is stripped. The trailing `_` delimiter is added automatically — passing `"user"`, `"user_"`, or `"user-"` yields the same result.
78
+ - `time:` (keyword, defaults to current ms): Integer milliseconds since Unix epoch. Range: `0` to `35_184_372_088_831` (Dec 12, 3084).
79
+ - `random:` (keyword, defaults to a secure 64-bit random Integer): non-negative Integer.
80
+
81
+ Raises `ArgumentError` for negative `time`, `time` exceeding the max, or negative `random`.
82
+
83
+ ### `Ashid.generate4(prefix = nil, random1:, random2:)`
84
+
85
+ Generate a random-only ID (UUIDv4 equivalent), without time-sortability. 26-char base, ~106 bits of entropy. Useful for tokens, secrets, or any case where unpredictability matters more than ordering.
86
+
87
+ ```ruby
88
+ Ashid.generate4 # => "14d2pf2dbsqqg0zvebn63pags1" (26 chars)
89
+ Ashid.generate4("tok") # => "tok_14d2pf2dbsqqg0zvebn63pags1"
90
+ ```
91
+
92
+ ### `Ashid.parse(id)`
93
+
94
+ Parse an ID into its components.
95
+
96
+ ```ruby
97
+ Ashid.parse("user_1je3kvrg000007fn17wx6b")
98
+ # => {prefix: "user_", encoded_timestamp: "1je3kvrg0", encoded_random: "00007fn17wx6b"}
99
+ ```
100
+
101
+ Raises `Ashid::InvalidIdError` for `nil`, empty, or malformed input.
102
+
103
+ ### `Ashid.prefix(id)`, `Ashid.timestamp(id)`, `Ashid.time(id)`, `Ashid.random(id)`, `Ashid.random_bytes(id)`
104
+
105
+ Convenience accessors:
106
+
107
+ ```ruby
108
+ id = Ashid.generate("user")
109
+
110
+ Ashid.prefix(id) # => "user_"
111
+ Ashid.timestamp(id) # => 1733140800000 (Integer ms)
112
+ Ashid.time(id) # => 2024-12-02 12:00:00 UTC (Time object)
113
+ Ashid.random(id) # => 8234567890123 (Integer)
114
+ Ashid.random_bytes(id) # => "\x00\x00..." (8-byte ASCII-8BIT String)
115
+ ```
116
+
117
+ ### `Ashid.valid?(id)`
118
+
119
+ Returns `true`/`false`. Never raises, even on `nil` or non-String input.
120
+
121
+ ```ruby
122
+ Ashid.valid?("user_1je3kvrg000007fn17wx6b") # => true
123
+ Ashid.valid?(nil) # => false
124
+ Ashid.valid?("garbage") # => false
125
+ ```
126
+
127
+ ### `Ashid.normalize(id)`
128
+
129
+ Canonicalize an ID: lowercase the prefix and resolve Crockford lookalikes (`I/L → 1`, `O → 0`, `U → V`).
130
+
131
+ ```ruby
132
+ Ashid.normalize("USER_IJE3KVRGOOOOO7FNI7WX6B")
133
+ # => "user_1je3kvrg000007fn17wx6b"
134
+ ```
135
+
136
+ ## Format
137
+
138
+ ```
139
+ [prefix_]?[timestamp][random]
140
+ ↓ ↓ ↓
141
+ user_ 1je3kvrg0 00007fn17wx6b
142
+ ```
143
+
144
+ - **With prefix**: variable-length timestamp + 13-char padded random.
145
+ - **Without prefix**: 9-char zero-padded timestamp + 13-char padded random = fixed 22 chars.
146
+ - **`generate4` (random-only)**: 13 + 13 = 26-char base.
147
+
148
+ **Crockford alphabet:** `0123456789abcdefghjkmnpqrstvwxyz`. Excludes `i`, `l`, `o`, `u`. On decode, lookalikes map: `I/L → 1`, `O → 0`, `U → V`.
149
+
150
+ ## Cross-language compatibility
151
+
152
+ `ashid` IDs round-trip byte-for-byte across:
153
+
154
+ - [ashid (TypeScript/JavaScript)](https://www.npmjs.com/package/ashid)
155
+ - [ashid (Kotlin/Java)](https://central.sonatype.com/artifact/agency.wilde/ashid)
156
+ - This Ruby gem
157
+
158
+ A test fixture in `test/fixtures/parity.json` verifies parity against IDs generated by the TS reference implementation.
159
+
160
+ ## Comparison
161
+
162
+ | Feature | ashid | UUID | nanoid | ULID | SecureRandom.uuid |
163
+ |---|---|---|---|---|---|
164
+ | Type prefixes | Built-in | No | No | No | No |
165
+ | Time-sortable | Yes | No | No | Yes | No |
166
+ | Human-readable encoding | Crockford Base32 | Hex | Base64 | Base32 | Hex |
167
+ | Case-insensitive | Yes | Yes | No | No | Yes |
168
+ | Lookalike correction | Yes (I→1, O→0) | No | No | No | No |
169
+ | Double-click selectable | Yes | No (hyphens) | Yes | Yes | No |
170
+ | URL-safe | Yes | Needs encoding | Yes | Yes | Needs encoding |
171
+ | Stdlib only | Yes | (gem) | (gem) | (gem) | Yes |
172
+
173
+ ## Inspired by
174
+
175
+ - [Stripe's ID format](https://stripe.com/docs/api) — the `sk_`, `pi_`, `cus_` prefix convention
176
+ - [Crockford Base32](https://www.crockford.com/base32.html) — human-friendly encoding
177
+ - [ULID](https://github.com/ulid/spec) — time-sortable IDs
178
+ - [TypeID](https://github.com/jetpack-io/typeid) — type-safe, K-sortable IDs
179
+
180
+ ## Credits
181
+
182
+ Original ashid library by **Dathan Guiley** at [Wilde Agency](https://wilde.agency). Ruby port by **Dan Kozlowski**.
183
+
184
+ ## License
185
+
186
+ MIT — see [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom" # used by secure_random (added in Task 7)
4
+
5
+ module Ashid
6
+ module Encoder
7
+ extend self
8
+
9
+ ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz".freeze
10
+
11
+ DECODE_MAP = begin
12
+ map = {}
13
+ ALPHABET.each_char.with_index do |c, i|
14
+ map[c] = i
15
+ map[c.upcase] = i
16
+ end
17
+ # Crockford lookalike corrections
18
+ map["O"] = map["o"] = 0
19
+ map["I"] = map["i"] = map["L"] = map["l"] = 1
20
+ map["U"] = map["u"] = 27 # maps to V's value
21
+ map.freeze
22
+ end
23
+ private_constant :DECODE_MAP
24
+
25
+ def encode(n, padded: false)
26
+ raise ArgumentError, "must be non-negative" if n < 0
27
+
28
+ str = encode_iterative(n)
29
+ padded ? str.rjust(13, "0") : str
30
+ end
31
+
32
+ def decode(str)
33
+ raise InvalidEncodingError, "empty string" if str.nil? || str.empty?
34
+
35
+ str.each_char.reduce(0) do |acc, ch|
36
+ v = DECODE_MAP[ch]
37
+ raise InvalidEncodingError, "invalid character: #{ch.inspect}" if v.nil?
38
+
39
+ acc * 32 + v
40
+ end
41
+ end
42
+
43
+ def secure_random(bits: 64)
44
+ raise ArgumentError, "bits must be a positive multiple of 8" unless bits.is_a?(Integer) && bits.positive? && (bits % 8).zero?
45
+
46
+ SecureRandom.bytes(bits / 8).bytes.reduce(0) { |acc, b| (acc << 8) | b }
47
+ end
48
+
49
+ def valid?(str)
50
+ return false unless str.is_a?(String)
51
+
52
+ decode(str)
53
+ true
54
+ rescue InvalidEncodingError
55
+ false
56
+ end
57
+
58
+ private
59
+
60
+ def encode_iterative(n)
61
+ return "0" if n.zero?
62
+
63
+ chars = []
64
+ while n > 0
65
+ chars.unshift(ALPHABET[n % 32])
66
+ n /= 32
67
+ end
68
+ chars.join
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ashid
4
+ class Error < StandardError; end
5
+ class InvalidIdError < Error; end
6
+ class InvalidEncodingError < Error; end
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ashid
4
+ VERSION = "0.1.0"
5
+ end
data/lib/ashid.rb ADDED
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ashid/version"
4
+ require_relative "ashid/error"
5
+ require_relative "ashid/encoder"
6
+
7
+ module Ashid
8
+ extend self
9
+
10
+ MAX_TIMESTAMP = 35_184_372_088_831
11
+ TIMESTAMP_ENCODED_LENGTH = 9
12
+ RANDOM_ENCODED_LENGTH = 13
13
+ STANDARD_BASE_LENGTH = 22
14
+ ASHID4_BASE_LENGTH = 26
15
+
16
+ def generate(prefix = nil, time: current_time_ms, random: Encoder.secure_random(bits: 64))
17
+ raise ArgumentError, "time must be non-negative" if time < 0
18
+ raise ArgumentError, "time must not exceed #{MAX_TIMESTAMP}" if time > MAX_TIMESTAMP
19
+ raise ArgumentError, "random must be non-negative" if random < 0
20
+
21
+ normalized = normalize_prefix(prefix)
22
+ base = build_base_id(normalized, time, random)
23
+ "#{normalized}#{base}"
24
+ end
25
+
26
+ def generate4(prefix = nil, random1: Encoder.secure_random(bits: 64), random2: Encoder.secure_random(bits: 64))
27
+ raise ArgumentError, "random1 must be non-negative" if random1 < 0
28
+ raise ArgumentError, "random2 must be non-negative" if random2 < 0
29
+
30
+ normalized = normalize_prefix(prefix)
31
+ base = "#{Encoder.encode(random1, padded: true)}#{Encoder.encode(random2, padded: true)}"
32
+ "#{normalized}#{base}"
33
+ end
34
+
35
+ def parse(id)
36
+ raise InvalidIdError, "id cannot be nil" if id.nil?
37
+ raise InvalidIdError, "id must be a String" unless id.is_a?(String)
38
+ raise InvalidIdError, "id cannot be empty" if id.empty?
39
+
40
+ prefix_length = 0
41
+ has_delimiter = false
42
+
43
+ id.each_char do |ch|
44
+ if ch.match?(/[a-zA-Z]/)
45
+ prefix_length += 1
46
+ elsif (ch == "_" || ch == "-") && prefix_length > 0
47
+ prefix_length += 1
48
+ has_delimiter = true
49
+ break
50
+ else
51
+ prefix_length = 0
52
+ break
53
+ end
54
+ end
55
+
56
+ prefix = has_delimiter ? id[0, prefix_length].sub(/-\z/, "_") : ""
57
+ base_id = has_delimiter ? id[prefix_length..] : id
58
+
59
+ raise InvalidIdError, "id must have a base ID" if base_id.nil? || base_id.empty?
60
+
61
+ encoded_timestamp, encoded_random = split_base(base_id, has_delimiter)
62
+
63
+ { prefix: prefix, encoded_timestamp: encoded_timestamp, encoded_random: encoded_random }
64
+ end
65
+
66
+ def prefix(id)
67
+ parse(id)[:prefix]
68
+ end
69
+
70
+ def timestamp(id)
71
+ Encoder.decode(parse(id)[:encoded_timestamp])
72
+ end
73
+
74
+ # Returns a Time. Note: ms-precision via float division to seconds; sub-ms detail is lost
75
+ # but ms is our resolution anyway.
76
+ def time(id)
77
+ Time.at(timestamp(id) / 1000.0)
78
+ end
79
+
80
+ def random(id)
81
+ Encoder.decode(parse(id)[:encoded_random])
82
+ end
83
+
84
+ def random_bytes(id)
85
+ [random(id).to_s(16).rjust(16, "0")].pack("H*")
86
+ end
87
+
88
+ def valid?(id)
89
+ return false unless id.is_a?(String) && !id.empty?
90
+
91
+ parsed = parse(id)
92
+ if !parsed[:prefix].empty? && parsed[:prefix] !~ /\A[a-z0-9]+_\z/
93
+ return false
94
+ end
95
+
96
+ Encoder.decode(parsed[:encoded_timestamp])
97
+ Encoder.decode(parsed[:encoded_random])
98
+ true
99
+ rescue Error
100
+ false
101
+ end
102
+
103
+ def normalize(id)
104
+ parsed = parse(id)
105
+ norm_prefix = parsed[:prefix].empty? ? nil : parsed[:prefix].downcase.chomp("_")
106
+ time_value = Encoder.decode(parsed[:encoded_timestamp])
107
+ random_value = Encoder.decode(parsed[:encoded_random])
108
+ generate(norm_prefix, time: time_value, random: random_value)
109
+ end
110
+
111
+ private
112
+
113
+ def normalize_prefix(prefix)
114
+ return nil if prefix.nil? || prefix.empty?
115
+
116
+ cleaned = prefix.gsub(/[^a-zA-Z0-9]/, "").downcase
117
+ cleaned.empty? ? nil : "#{cleaned}_"
118
+ end
119
+
120
+ def build_base_id(normalized_prefix, time, random)
121
+ if normalized_prefix.nil?
122
+ # No prefix: fixed 22-char (timestamp padded to 9, random padded to 13)
123
+ time_part = Encoder.encode(time).rjust(TIMESTAMP_ENCODED_LENGTH, "0")
124
+ random_part = Encoder.encode(random, padded: true)
125
+ "#{time_part}#{random_part}"
126
+ elsif time > 0
127
+ # Variable length, padded random
128
+ "#{Encoder.encode(time)}#{Encoder.encode(random, padded: true)}"
129
+ else
130
+ # time == 0: omit timestamp, encode random unpadded
131
+ Encoder.encode(random)
132
+ end
133
+ end
134
+
135
+ def current_time_ms
136
+ (Process.clock_gettime(Process::CLOCK_REALTIME) * 1000).to_i
137
+ end
138
+
139
+ def split_base(base_id, has_delimiter)
140
+ if has_delimiter
141
+ if base_id.length <= RANDOM_ENCODED_LENGTH
142
+ ["0", base_id]
143
+ else
144
+ [base_id[0...-RANDOM_ENCODED_LENGTH], base_id[-RANDOM_ENCODED_LENGTH..]]
145
+ end
146
+ elsif base_id.length == ASHID4_BASE_LENGTH
147
+ [base_id[0, RANDOM_ENCODED_LENGTH], base_id[RANDOM_ENCODED_LENGTH..]]
148
+ elsif base_id.length == STANDARD_BASE_LENGTH
149
+ [base_id[0, TIMESTAMP_ENCODED_LENGTH], base_id[TIMESTAMP_ENCODED_LENGTH..]]
150
+ else
151
+ raise InvalidIdError,
152
+ "base ID must be #{STANDARD_BASE_LENGTH} or #{ASHID4_BASE_LENGTH} chars without delimiter (got #{base_id.length})"
153
+ end
154
+ end
155
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ashid
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dan Kozlowski
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-05-06 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.20'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.20'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ description: 'Ruby port of ashid: time-sortable unique IDs with optional type prefixes,
41
+ encoded in Crockford Base32. Wire-compatible with the TypeScript and Kotlin implementations.'
42
+ email:
43
+ - dakozlowski@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - CHANGELOG.md
49
+ - LICENSE.txt
50
+ - README.md
51
+ - lib/ashid.rb
52
+ - lib/ashid/encoder.rb
53
+ - lib/ashid/error.rb
54
+ - lib/ashid/version.rb
55
+ homepage: https://github.com/dankozlowski/ashid-ruby
56
+ licenses:
57
+ - MIT
58
+ metadata:
59
+ homepage_uri: https://github.com/dankozlowski/ashid-ruby
60
+ source_code_uri: https://github.com/dankozlowski/ashid-ruby
61
+ changelog_uri: https://github.com/dankozlowski/ashid-ruby/blob/main/CHANGELOG.md
62
+ bug_tracker_uri: https://github.com/dankozlowski/ashid-ruby/issues
63
+ rubygems_mfa_required: 'true'
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '3.0'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubygems_version: 3.6.2
79
+ specification_version: 4
80
+ summary: Time-sortable unique identifiers with type prefixes
81
+ test_files: []