exid 0.1.2 → 0.1.3
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/.ruby-version +1 -0
- data/Dockerfile +3 -0
- data/README.md +79 -47
- data/docker-compose.yml +20 -0
- data/lib/exid/base62.rb +9 -1
- data/lib/exid/coder.rb +2 -2
- data/lib/exid/error.rb +5 -0
- data/lib/exid/record.rb +9 -7
- data/lib/exid/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ca16f83a2ab050b505cdb0f774c7f147d5ba07bd8ebfa823f7b50791dcb8af5d
|
4
|
+
data.tar.gz: 4bcf07c0a4a1a61305416f13ddf0165292e6f01b2c379a21ca03492ec36a052e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: acbe278cae8f90cde6d27a839c9bbf76dfcda618fa5244c6d79765445fc506de7af1171540960a50821637c07b0d979d869ea35e19b49e93b4ee95af0a10e22a
|
7
|
+
data.tar.gz: 1e5878b5c78e5a91494ef8918a5edad2d3c245fed36d50b0298951d06da366c0c7405ca8e24eb01dec3dcf719bf02ae4a5ef5f9096f357ce7d6744dfad71d608
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.4.4
|
data/Dockerfile
ADDED
data/README.md
CHANGED
@@ -1,84 +1,116 @@
|
|
1
|
-
# Exid
|
1
|
+
# Exid 
|
2
2
|
|
3
|
-
|
3
|
+
A Ruby gem for implementing human-friendly, prefixed identifiers for records using Base62-encoded UUIDs.
|
4
4
|
|
5
|
-
|
6
|
-
identifiers for records.
|
5
|
+
## Overview
|
7
6
|
|
8
|
-
|
9
|
-
returns an "external ID", composed of this prefix and zero-padded
|
10
|
-
Base62-encoded UUID.
|
7
|
+
Exid provides helper methods to create external, prefixed identifiers for your records, following the pattern popularized by Stripe's API.
|
11
8
|
|
12
|
-
|
9
|
+
Similar to Stripe's IDs (like `cus_12345` for customers or `prod_67890` for products), Exid generates readable, prefixed identifiers that:
|
13
10
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
11
|
+
- Are human-friendly and can be safely exposed in URLs
|
12
|
+
- Contain semantic prefixes that indicate resource type
|
13
|
+
- Hide internal database IDs
|
14
|
+
- Maintain global uniqueness
|
15
|
+
- Are collision-resistant
|
16
|
+
- Have constant length for a consistent user experience
|
17
|
+
|
18
|
+
The core `Exid::Coder.encode` method accepts a string prefix with a UUID and returns an "external ID" composed of the prefix and a zero-padded **Base62-encoded UUID**.
|
19
|
+
|
20
|
+
For example:
|
21
|
+
```
|
22
|
+
prg, 018977bb-02f0-729c-8c00-2f384eccb763 => prg_02TOxMzOS0VaLzYiS3NPd9
|
23
|
+
```
|
24
|
+
|
25
|
+
### Resources
|
26
|
+
|
27
|
+
For more information on this approach:
|
28
|
+
- [Designing APIs for Humans: Object IDs](https://dev.to/stripe/designing-apis-for-humans-object-ids-3o5a)
|
29
|
+
- [Prefixed Base62 UUIDv7 Object IDs with Ecto](https://danschultzer.com/posts/prefixed-base62-uuidv7-object-ids-with-ecto)
|
30
|
+
- [Friendly IDs for Ruby on Rails](https://dev.to/drnic/friendly-ids-for-ruby-on-rails-1c8p)
|
31
|
+
- [Prefixed IDs](https://github.com/excid3/prefixed_ids)
|
32
|
+
- [UUID7 Ruby](https://github.com/sprql/uuid7-ruby)
|
33
|
+
- [Base62 Ruby](https://github.com/steventen/base62-rb)
|
34
|
+
|
35
|
+
## Installation
|
36
|
+
|
37
|
+
Add this line to your application's Gemfile:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
gem "exid"
|
41
|
+
```
|
42
|
+
|
43
|
+
Then execute:
|
44
|
+
|
45
|
+
```
|
46
|
+
$ bundle install
|
47
|
+
```
|
21
48
|
|
22
49
|
## Usage
|
23
50
|
|
24
|
-
|
25
|
-
|
51
|
+
### Basic Setup
|
52
|
+
|
53
|
+
Add a UUID or (preferably) UUIDv7 field to your model and include the helper module. Pass a prefix (String) and a field name (Symbol) to the `Exid::Record.new` method:
|
26
54
|
|
27
55
|
```ruby
|
28
56
|
class User < ApplicationRecord
|
29
57
|
include Exid::Record.new("user", :uuid)
|
30
58
|
|
31
|
-
# Optional, but recommended
|
32
|
-
# identier.
|
33
|
-
|
59
|
+
# Optional, but recommended: Use the external ID as the primary object identifier
|
34
60
|
def to_param = exid_value
|
35
61
|
end
|
36
62
|
```
|
37
63
|
|
38
|
-
That's all
|
64
|
+
That's all! This adds several helper methods to your model.
|
65
|
+
|
66
|
+
### Example
|
39
67
|
|
40
68
|
```ruby
|
69
|
+
# Create a record with a UUID
|
41
70
|
user = User.create!(uuid: "018977bb-02f0-729c-8c00-2f384eccb763")
|
71
|
+
|
72
|
+
# Access the methods
|
73
|
+
user.exid_value # => "user_02TOxMzOS0VaLzYiS3NPd9"
|
74
|
+
user.exid_prefix_name # => "user"
|
75
|
+
user.exid_field # => :uuid
|
42
76
|
```
|
43
77
|
|
44
|
-
|
78
|
+
The `exid_handle` instance method returns the last 10 characters of the identifier. This is useful for displaying a distinguishing identifier in the UI. When using UUID7, the first few characters derive from timestamps and will be similar for objects created at the same time. You can pass an integer argument to get a specific number of trailing characters.
|
45
79
|
|
46
80
|
```ruby
|
47
|
-
user.
|
81
|
+
user.exid_handle # => "OBtqZqRhLm"
|
82
|
+
user.exid_handle(6) # => "ZqRhLm"
|
83
|
+
```
|
84
|
+
|
85
|
+
### Loading Records by External ID
|
48
86
|
|
49
|
-
|
87
|
+
Use the class method `exid_loader` to load a record using its external ID:
|
50
88
|
|
51
|
-
|
89
|
+
```ruby
|
90
|
+
User.exid_loader("user_02TOxMzOS0VaLzYiS3NPd9")
|
91
|
+
# Raises ActiveRecord::RecordNotFound if not found
|
92
|
+
# Raises NoMatchingPatternError if ID format is invalid
|
52
93
|
```
|
53
94
|
|
54
|
-
The `
|
55
|
-
identifier. This might be useful for displaying in the UI as distinguishing
|
56
|
-
identifier. If the UUID7 is used as the identifier, the first few characters
|
57
|
-
are not random. They come from the timestamp, so they will be the same for most
|
58
|
-
objects created at the same time. Pass integer as the argument to get the last
|
59
|
-
N characters.
|
95
|
+
The `Exid::Record` module also provides global loading methods that mimic Rails `GlobalID` functionality:
|
60
96
|
|
61
97
|
```ruby
|
62
|
-
|
63
|
-
|
64
|
-
user.exid_handle(6) # => "ZqRhLm"
|
98
|
+
Exid::Record.fetch!("pref_02WoeojY8dqVYcAhs321rm") # Raises exception if not found
|
99
|
+
Exid::Record.fetch("pref_02WoeojY8dqVYcAhs321rm") # Returns nil if not found
|
65
100
|
```
|
66
101
|
|
67
|
-
|
68
|
-
records. This is another way to mimic Rails `GlobalID`. Warning: Steer
|
69
|
-
away from using this as default way to load records using user supplied
|
70
|
-
identifiers. User might replace the identifier with other record which might
|
71
|
-
lead to unexpected results and security issues.
|
102
|
+
> **⚠️ Security Warning**: Exercise caution when using global loading methods with user-supplied identifiers, as this could lead to unexpected results or security issues if users substitute identifiers.
|
72
103
|
|
73
|
-
|
74
|
-
`fetch!` variant will use Rails 7.1+ `sole` under the hood and raise an
|
75
|
-
exception if the record is not found (or if more than one record is found).
|
104
|
+
**Note**: When using this gem in Rails development mode (with `eager_load` set to `false`), ensure the model class is referenced before calling `.fetch` or `.fetch!`. This ensures the prefix is added to the global registry so the loader can identify which class is associated with the prefix.
|
76
105
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
106
|
+
## Contributing
|
107
|
+
|
108
|
+
1. Fork the repository
|
109
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
110
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
111
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
112
|
+
5. Create a new Pull Request
|
81
113
|
|
82
114
|
## License
|
83
115
|
|
84
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
116
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/docker-compose.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
---
|
2
|
+
name: exid
|
3
|
+
|
4
|
+
x-llm: &llm
|
5
|
+
build:
|
6
|
+
context: .
|
7
|
+
dockerfile: Dockerfile
|
8
|
+
target: node-base
|
9
|
+
image: claude
|
10
|
+
tmpfs:
|
11
|
+
- /tmp:mode=1777
|
12
|
+
volumes:
|
13
|
+
- .:/app
|
14
|
+
- ~/.claude.json:/root/.claude.json
|
15
|
+
|
16
|
+
services:
|
17
|
+
ai:
|
18
|
+
<<: *llm
|
19
|
+
stdin_open: true
|
20
|
+
tty: true
|
data/lib/exid/base62.rb
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
module Exid
|
4
4
|
class Base62
|
5
|
+
class DecodeError < Error; end
|
6
|
+
|
5
7
|
CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
6
8
|
BASE = CHARS.length
|
7
9
|
CHARS_HASH = CHARS.each_char.zip(0...BASE).to_h
|
@@ -22,7 +24,13 @@ module Exid
|
|
22
24
|
def self.decode(str)
|
23
25
|
max = str.length - 1
|
24
26
|
str.each_char.zip(0..max).reduce(0) do |acc, (char, index)|
|
25
|
-
|
27
|
+
character = CHARS_HASH[char]
|
28
|
+
|
29
|
+
if character.nil?
|
30
|
+
raise DecodeError, "Invalid character '#{char}' in string '#{str}'"
|
31
|
+
end
|
32
|
+
|
33
|
+
acc + (character * (BASE**(max - index)))
|
26
34
|
end
|
27
35
|
end
|
28
36
|
end
|
data/lib/exid/coder.rb
CHANGED
@@ -33,7 +33,7 @@ module Exid
|
|
33
33
|
def self.encode(prefix, uuid)
|
34
34
|
[
|
35
35
|
prefix.to_s,
|
36
|
-
Base62.encode(uuid.delete("-").hex)
|
36
|
+
Base62.encode(uuid.delete("-").hex)
|
37
37
|
].join("_")
|
38
38
|
end
|
39
39
|
|
@@ -48,7 +48,7 @@ module Exid
|
|
48
48
|
|
49
49
|
Result.new(
|
50
50
|
prefix,
|
51
|
-
[hex[0..7], hex[8..11], hex[12..15], hex[16..19], hex[20..31]].join("-")
|
51
|
+
[hex[0..7], hex[8..11], hex[12..15], hex[16..19], hex[20..31]].join("-")
|
52
52
|
)
|
53
53
|
end
|
54
54
|
end
|
data/lib/exid/error.rb
ADDED
data/lib/exid/record.rb
CHANGED
@@ -5,6 +5,7 @@ module Exid
|
|
5
5
|
Entry =
|
6
6
|
Data.define(:prefix, :field, :klass) do
|
7
7
|
def ==(other) = prefix == other.prefix
|
8
|
+
|
8
9
|
def hash = prefix.hash
|
9
10
|
|
10
11
|
alias_method :eql?, :==
|
@@ -13,17 +14,17 @@ module Exid
|
|
13
14
|
@registered_modules = Set.new
|
14
15
|
|
15
16
|
def included(base)
|
16
|
-
base.
|
17
|
-
base.
|
18
|
-
base.
|
19
|
-
base.
|
17
|
+
base.public_send(:include, @module_value)
|
18
|
+
base.public_send(:include, @module_shared)
|
19
|
+
base.public_send(:extend, @module_shared)
|
20
|
+
base.public_send(:extend, @module_static)
|
20
21
|
|
21
22
|
self.class.register_module(
|
22
23
|
Entry.new(
|
23
24
|
prefix: base.exid_prefix_name,
|
24
25
|
field: base.exid_field,
|
25
|
-
klass: base
|
26
|
-
)
|
26
|
+
klass: base
|
27
|
+
)
|
27
28
|
)
|
28
29
|
end
|
29
30
|
|
@@ -74,6 +75,7 @@ module Exid
|
|
74
75
|
end
|
75
76
|
|
76
77
|
def self.fetch(eid) = finder(eid).first
|
78
|
+
|
77
79
|
def self.fetch!(eid) = finder(eid).sole
|
78
80
|
|
79
81
|
private
|
@@ -90,7 +92,7 @@ module Exid
|
|
90
92
|
# display only, do not use this to fetch records, etc.
|
91
93
|
|
92
94
|
define_method :exid_handle do |amount = 10|
|
93
|
-
exid_value.split("_").last[-amount
|
95
|
+
exid_value.split("_").last[-amount..]
|
94
96
|
end
|
95
97
|
end
|
96
98
|
end
|
data/lib/exid/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: exid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Doe
|
@@ -31,12 +31,16 @@ extensions: []
|
|
31
31
|
extra_rdoc_files: []
|
32
32
|
files:
|
33
33
|
- ".rspec"
|
34
|
+
- ".ruby-version"
|
35
|
+
- Dockerfile
|
34
36
|
- LICENSE.txt
|
35
37
|
- README.md
|
36
38
|
- Rakefile
|
39
|
+
- docker-compose.yml
|
37
40
|
- lib/exid.rb
|
38
41
|
- lib/exid/base62.rb
|
39
42
|
- lib/exid/coder.rb
|
43
|
+
- lib/exid/error.rb
|
40
44
|
- lib/exid/record.rb
|
41
45
|
- lib/exid/version.rb
|
42
46
|
- sig/exid.rbs
|
@@ -59,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
59
63
|
- !ruby/object:Gem::Version
|
60
64
|
version: '0'
|
61
65
|
requirements: []
|
62
|
-
rubygems_version: 3.6.
|
66
|
+
rubygems_version: 3.6.9
|
63
67
|
specification_version: 4
|
64
68
|
summary: Easy External identifier management for models
|
65
69
|
test_files: []
|