exid 0.1.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0b51f276cf5b4bac2e662b192b8410ce19c76e04aa93e1b4b6daa58fba00ddb
4
- data.tar.gz: 4bf5b946a071e7eec2ce02f7bda93089f514f18daa1a858a58585952eea7c153
3
+ metadata.gz: ca16f83a2ab050b505cdb0f774c7f147d5ba07bd8ebfa823f7b50791dcb8af5d
4
+ data.tar.gz: 4bcf07c0a4a1a61305416f13ddf0165292e6f01b2c379a21ca03492ec36a052e
5
5
  SHA512:
6
- metadata.gz: 472e3a02ace26c1dd4acbf0109a3033f7273312fde90370bcbbe560416293e8bc51178c6c3859d31d4c46ee1c580f41213137091803c3a5f8d82630733f430ec
7
- data.tar.gz: e8cec05ea307a1c28479135758b5705b0b5fd993a2aa6ee51f937e1fb6ba79a947c5b810487b6027de9a8639dea5e05da9ad479222bfe2c62c890391f492d0dc
6
+ metadata.gz: acbe278cae8f90cde6d27a839c9bbf76dfcda618fa5244c6d79765445fc506de7af1171540960a50821637c07b0d979d869ea35e19b49e93b4ee95af0a10e22a
7
+ data.tar.gz: 1e5878b5c78e5a91494ef8918a5edad2d3c245fed36d50b0298951d06da366c0c7405ca8e24eb01dec3dcf719bf02ae4a5ef5f9096f357ce7d6744dfad71d608
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.4.4
data/Dockerfile ADDED
@@ -0,0 +1,3 @@
1
+ FROM node:20-bookworm AS node-base
2
+ WORKDIR /app
3
+ RUN npm install -g @anthropic-ai/claude-code
data/README.md CHANGED
@@ -1,84 +1,116 @@
1
- # Exid
1
+ # Exid ![CI](https://github.com/marzdrel/exid/actions/workflows/ci.yml/badge.svg)
2
2
 
3
- **!! Warning: Documentation is not complete yet. Work in progress**
3
+ A Ruby gem for implementing human-friendly, prefixed identifiers for records using Base62-encoded UUIDs.
4
4
 
5
- This gem implements helper methods for implementing External, Prefixed
6
- identifiers for records.
5
+ ## Overview
7
6
 
8
- Core `Exid::Coder.encode` accepts a string prefix with an UUID and
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
- For example: `prg, 018977bb-02f0-729c-8c00-2f384eccb763` => `prg_02TOxMzOS0VaLzYiS3NPd9`
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
- See more:
15
- - https://dev.to/stripe/designing-apis-for-humans-object-ids-3o5a
16
- - https://danschultzer.com/posts/prefixed-base62-uuidv7-object-ids-with-ecto
17
- - https://dev.to/drnic/friendly-ids-for-ruby-on-rails-1c8p
18
- - https://github.com/excid3/prefixed_ids
19
- - https://github.com/sprql/uuid7-ruby
20
- - https://github.com/steventen/base62-rb
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
- Add a UUID or (preferably) UUIDv7 to your model include a helper module. Pass a
25
- prefix (String) and a field name (Symbol) to the `Exid::Record.new` method.
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. Use the Exid value as the primary object
32
- # identier.
33
-
34
- def to_param = prefix_eid_value
59
+ # Optional, but recommended: Use the external ID as the primary object identifier
60
+ def to_param = exid_value
35
61
  end
36
62
  ```
37
63
 
38
- That's all. This will add certain class and instance methods to your object.
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
- Following methods are now available on the instance class.
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.prefix_eid_value # => "user_02TOxMzOS0VaLzYiS3NPd9"
81
+ user.exid_handle # => "OBtqZqRhLm"
82
+ user.exid_handle(6) # => "ZqRhLm"
83
+ ```
48
84
 
49
- user.prefix_eid_prefix_name # => "user"
85
+ ### Loading Records by External ID
50
86
 
51
- user.prefix_eid_field # => :uuid
87
+ Use the class method `exid_loader` to load a record using its external ID:
88
+
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 `prefix_eid_handle` instanec method simply returns last 10 characters of
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
- user.prefix_eid_handle # => "OBtqZqRhLm"
63
-
64
- user.prefix_eid_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
- The `Exid::Record` also offers couple of instance methods designed load
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
- The `fetch` class method will return the record or nil if not found. The
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
- ```ruby
78
- Exid::Record.fetch!("pref_02WoeojY8dqVYcAhs321rm")
79
- Exid::Record.fetch("pref_02WoeojY8dqVYcAhs321rm")
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).
@@ -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
- acc + (CHARS_HASH[char] * (BASE**(max - index)))
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exid
4
+ class Error < StandardError; end
5
+ end
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.send(:include, @module_value)
17
- base.send(:include, @module_shared)
18
- base.send(:extend, @module_shared)
19
- base.send(:extend, @module_static)
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
- prefix: base.prefix_eid_prefix_name,
24
- field: base.prefix_eid_field,
25
- klass: base,
26
- ),
24
+ prefix: base.exid_prefix_name,
25
+ field: base.exid_field,
26
+ klass: base
27
+ )
27
28
  )
28
29
  end
29
30
 
@@ -74,13 +75,14 @@ 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
80
82
 
81
83
  def build_module_value(prefix, field)
82
84
  Module.new do
83
- define_method :prefix_eid_value do
85
+ define_method :exid_value do
84
86
  Coder.encode(prefix, send(field))
85
87
  end
86
88
 
@@ -89,19 +91,19 @@ module Exid
89
91
  # last bytes of encoded of UUID7 is more likely to be unique. This for
90
92
  # display only, do not use this to fetch records, etc.
91
93
 
92
- define_method :prefix_eid_handle do |amount = 10|
93
- prefix_eid_value.split("_").last[-amount..-1]
94
+ define_method :exid_handle do |amount = 10|
95
+ exid_value.split("_").last[-amount..]
94
96
  end
95
97
  end
96
98
  end
97
99
 
98
100
  def build_module_shared(prefix, field)
99
101
  Module.new do
100
- define_method :prefix_eid_prefix_name do
102
+ define_method :exid_prefix_name do
101
103
  prefix
102
104
  end
103
105
 
104
- define_method :prefix_eid_field do
106
+ define_method :exid_field do
105
107
  field
106
108
  end
107
109
  end
@@ -109,7 +111,7 @@ module Exid
109
111
 
110
112
  def build_module_static(prefix, field)
111
113
  Module.new do
112
- define_method :prefix_eid_loader do |eid|
114
+ define_method :exid_loader do |eid|
113
115
  Coder.decode(eid) => ^prefix, value
114
116
  find_sole_by(field => value)
115
117
  end
data/lib/exid/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Exid
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
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.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.7
66
+ rubygems_version: 3.6.9
63
67
  specification_version: 4
64
68
  summary: Easy External identifier management for models
65
69
  test_files: []