neat_ids 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/MIT-LICENSE +20 -0
- data/README.md +118 -0
- data/Rakefile +18 -0
- data/lib/neat_ids/engine.rb +9 -0
- data/lib/neat_ids/neat_id.rb +59 -0
- data/lib/neat_ids/version.rb +3 -0
- data/lib/neat_ids.rb +133 -0
- metadata +78 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 408232bf986163d716c24d7d03891a3dec78f8da54c95eabf057b759c009308b
|
4
|
+
data.tar.gz: 3c88987f733c6293133bdb7247a93f68c2a58e14e416b5f9a57593a6660298ba
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 14202c84dbecce2b0c1a4d81f23469ede039cb90e58223d04876c143bdcc464bb920b2a41fa57aa7b7623223b9f655644a050b8ecb9dc3d1b10fe28b5af712bb
|
7
|
+
data.tar.gz: 6f13feb0c9cb862c767dc20d2cec62b8a54a79bf21dd1c22f934e1d42120d5452f4d73de402030c3c8ae78f282ce80f63dea3d295a0f73b0fd1349095f07c1ff
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2021 Chris Oliver
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
# NeatIds
|
2
|
+
Generate neat, Stripe-style prefixed IDs for your models with [Sqids](https://sqids.org/ruby). Works with numeric OR UUID-esque primary keys!
|
3
|
+
|
4
|
+
Heavily inspired by [`prefixed_ids`](https://github.com/excid3/prefixed_ids)
|
5
|
+
|
6
|
+
## Usage
|
7
|
+
How to use my plugin.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem "neat_ids"
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
```bash
|
18
|
+
$ bundle
|
19
|
+
```
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
```bash
|
23
|
+
$ gem install neat_ids
|
24
|
+
```
|
25
|
+
|
26
|
+
Add `has_neat_id :my_prefix` to your models (before you define any associations!!) to autogenerate Neat IDs.
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
class User < ApplicationRecord
|
30
|
+
has_neat_id :user
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
34
|
+
### Neat ID Param
|
35
|
+
|
36
|
+
By default, Neat IDs overrides `to_param` in the model to use Neat IDs.
|
37
|
+
|
38
|
+
To get the Neat ID for a record:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
@user.to_param
|
42
|
+
#=> "user_12345abcd"
|
43
|
+
```
|
44
|
+
|
45
|
+
If `to_param` override is disabled:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
@user.neat_id
|
49
|
+
#=> "user_12345abcd"
|
50
|
+
```
|
51
|
+
|
52
|
+
#### Query by Neat ID
|
53
|
+
|
54
|
+
By default, neat_ids overrides `find` and `to_param` to seamlessly URLs automatically.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
User.first.to_param
|
58
|
+
#=> "user_5vJjbzXq9KrLEMm32iAnOP0xGDYk6dpe"
|
59
|
+
|
60
|
+
User.find("user_5vJjbzXq9KrLEMm32iAnOP0xGDYk6dpe")
|
61
|
+
#=> #<User>
|
62
|
+
```
|
63
|
+
|
64
|
+
> [!NOTE]
|
65
|
+
> `find` still finds records by primary key. For example, `User.find(1)` still works.
|
66
|
+
|
67
|
+
You can also use `find_by_neat_id` or `find_by_neat_id!` when the `find` override is disabled:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
User.find_by_neat_id("user_5vJjbzXq9KrLEMm32iAnOP0xGDYk6dpe") # Returns a User or nil
|
71
|
+
User.find_by_neat_id!("user_5vJjbzXq9KrLEMm32iAnOP0xGDYk6dpe") # Raises an exception if not found
|
72
|
+
```
|
73
|
+
|
74
|
+
To disable `find` and `to_param` overrides, pass the following options:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
class User < ApplicationRecord
|
78
|
+
has_neat_id :user, override_find: false, override_param: false
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
> [!NOTE]
|
83
|
+
> If you're aiming to masking primary key ID for security reasons, make sure to use `find_by_neat_id`.
|
84
|
+
|
85
|
+
### Find Any Model By Neat ID
|
86
|
+
|
87
|
+
Imagine you have a Neat ID but you don't know which model it belongs to:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
NeatIds.find("user_5vJjbzXq9KrLEMm3")
|
91
|
+
#=> #<User>
|
92
|
+
|
93
|
+
NeatIds.find("acct_2iAnOP0xGDYk6dpe")
|
94
|
+
#=> #<Account>
|
95
|
+
```
|
96
|
+
|
97
|
+
This works similarly to GlobalIDs.
|
98
|
+
|
99
|
+
### Customizing Neat IDs
|
100
|
+
|
101
|
+
You can customize the prefix, length, and attribute name for NeatIds.
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
class Account < ApplicationRecord
|
105
|
+
has_neat_id :acct, minimum_length: 32, override_find: false, override_param: false, fallback: false
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
By default, `find` will accept both Neat IDs and regular IDs. Setting `fallback: false` will disable finding by regular IDs and will only allow Neat IDs.
|
110
|
+
|
111
|
+
## Contributing
|
112
|
+
|
113
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/braddoeswebdev/neat_ids.
|
114
|
+
|
115
|
+
Contributors are expected to adhere to the [code of conduct](https://github.com/braddoeswebdev/neat_ids/blob/master/CODE_OF_CONDUCT.md).
|
116
|
+
|
117
|
+
## License
|
118
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
|
3
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
4
|
+
load "rails/tasks/engine.rake"
|
5
|
+
|
6
|
+
load "rails/tasks/statistics.rake"
|
7
|
+
|
8
|
+
require "bundler/gem_tasks"
|
9
|
+
|
10
|
+
require "rake/testtask"
|
11
|
+
|
12
|
+
Rake::TestTask.new(:test) do |t|
|
13
|
+
t.libs << "test"
|
14
|
+
t.pattern = "test/**/*_test.rb"
|
15
|
+
t.verbose = false
|
16
|
+
end
|
17
|
+
|
18
|
+
task default: :test
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module NeatIds
|
2
|
+
class NeatId
|
3
|
+
attr_reader :sqids, :prefix
|
4
|
+
|
5
|
+
TOKEN = 123
|
6
|
+
|
7
|
+
def initialize(model, prefix, min_length: NeatIds.minimum_length, alphabet: NeatIds.alphabet, delimiter: NeatIds.delimiter, **options)
|
8
|
+
@prefix = prefix.to_s
|
9
|
+
@delimiter = delimiter.to_s
|
10
|
+
@sqids = Sqids.new( min_length: min_length, alphabet: alphabet)
|
11
|
+
end
|
12
|
+
|
13
|
+
def encode(id)
|
14
|
+
return if id.nil?
|
15
|
+
if uuid?(id)
|
16
|
+
nums = uuid_to_ints(id)
|
17
|
+
return @prefix + @delimiter + @sqids.encode([TOKEN] + nums)
|
18
|
+
end
|
19
|
+
@prefix + @delimiter + @sqids.encode([TOKEN] + Array.wrap(id))
|
20
|
+
end
|
21
|
+
|
22
|
+
def decode(id, fallback: false)
|
23
|
+
fallback_value = fallback ? id : nil
|
24
|
+
_, id_without_prefix = NeatIds.split_id(id, @delimiter)
|
25
|
+
decoded_hashid = @sqids.decode(id_without_prefix)
|
26
|
+
|
27
|
+
if !valid?(decoded_hashid)
|
28
|
+
fallback_value
|
29
|
+
else
|
30
|
+
_, *ids = decoded_hashid
|
31
|
+
if ids.size == 4 && ids.all? { |n| n.is_a?(Integer) && n >= 0 && n <= 0xFFFFFFFF }
|
32
|
+
ints_to_uuid(ids)
|
33
|
+
else
|
34
|
+
(ids.size == 1) ? ids.first : ids
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def valid?(decoded_hashid)
|
42
|
+
decoded_hashid.size >= 2 && decoded_hashid.first == TOKEN
|
43
|
+
end
|
44
|
+
|
45
|
+
def uuid?(id)
|
46
|
+
id.is_a?(String) && id.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/)
|
47
|
+
end
|
48
|
+
|
49
|
+
def uuid_to_ints(uuid)
|
50
|
+
hex = uuid.delete('-')
|
51
|
+
[0, 8, 16, 24].map { |i| hex[i, 8].to_i(16) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def ints_to_uuid(ints)
|
55
|
+
hex = ints.map { |n| n.to_s(16).rjust(8, '0') }.join
|
56
|
+
"#{hex[0,8]}-#{hex[8,4]}-#{hex[12,4]}-#{hex[16,4]}-#{hex[20,12]}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/neat_ids.rb
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
require "neat_ids/version"
|
2
|
+
require "neat_ids/engine"
|
3
|
+
|
4
|
+
require "sqids"
|
5
|
+
|
6
|
+
module NeatIds
|
7
|
+
class Error < StandardError; end
|
8
|
+
|
9
|
+
autoload :NeatId, "neat_ids/neat_id"
|
10
|
+
|
11
|
+
mattr_accessor :delimiter, default: "_"
|
12
|
+
mattr_accessor :alphabet, default: "abcdefghijklmnopqrstuvwxyz1234567890"
|
13
|
+
mattr_accessor :minimum_length, default: 24
|
14
|
+
|
15
|
+
mattr_accessor :models, default: {}
|
16
|
+
|
17
|
+
def self.find(neat_id)
|
18
|
+
prefix, _ = split_id(neat_id)
|
19
|
+
models.fetch(prefix).find_by_neat_id(neat_id)
|
20
|
+
rescue KeyError
|
21
|
+
raise Error, "Unable to find model with prefix `#{prefix}`. Available prefixes are: #{models.keys.join(", ")}"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Splits a prefixed ID into its prefix and ID
|
25
|
+
def self.split_id(neat_id, delimiter = NeatIds.delimiter)
|
26
|
+
prefix, _, id = neat_id.to_s.rpartition(delimiter)
|
27
|
+
[prefix, id]
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.register_prefix(prefix, model:)
|
31
|
+
if (existing_model = NeatIds.models[prefix]) && existing_model != model
|
32
|
+
raise Error, "Prefix #{prefix} already defined for model #{model}"
|
33
|
+
end
|
34
|
+
|
35
|
+
NeatIds.models[prefix] = model
|
36
|
+
end
|
37
|
+
|
38
|
+
# Adds `has_neat_id` method
|
39
|
+
module Rails
|
40
|
+
extend ActiveSupport::Concern
|
41
|
+
|
42
|
+
included do
|
43
|
+
class_attribute :_neat_id
|
44
|
+
class_attribute :_neat_id_fallback
|
45
|
+
end
|
46
|
+
|
47
|
+
class_methods do
|
48
|
+
def has_neat_id(prefix, override_find: true, override_param: true, fallback: true, **options)
|
49
|
+
include Attribute
|
50
|
+
include Finder if override_find
|
51
|
+
include ToParam if override_param
|
52
|
+
self._neat_id = NeatId.new(self, prefix, **options)
|
53
|
+
self._neat_id_fallback = fallback
|
54
|
+
|
55
|
+
# Register with NeatIds to support NeatIds#find
|
56
|
+
NeatIds.register_prefix(prefix.to_s, model: self)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Included when a module uses `has_neat_id`
|
62
|
+
module Attribute
|
63
|
+
extend ActiveSupport::Concern
|
64
|
+
|
65
|
+
class_methods do
|
66
|
+
def find_by_neat_id(id)
|
67
|
+
find_by(id: _neat_id.decode(id))
|
68
|
+
end
|
69
|
+
|
70
|
+
def find_by_neat_id!(id)
|
71
|
+
find_by!(id: _neat_id.decode(id))
|
72
|
+
end
|
73
|
+
|
74
|
+
def neat_id(id)
|
75
|
+
_neat_id.encode(id)
|
76
|
+
end
|
77
|
+
|
78
|
+
def neat_ids(ids)
|
79
|
+
ids.map { |id| neat_id(id) }
|
80
|
+
end
|
81
|
+
|
82
|
+
def decode_neat_id(id)
|
83
|
+
_neat_id.decode(id)
|
84
|
+
end
|
85
|
+
|
86
|
+
def decode_neat_ids(ids)
|
87
|
+
ids.map { |id| decode_neat_id(id) }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def neat_id
|
92
|
+
_neat_id.encode(id)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
module Finder
|
97
|
+
extend ActiveSupport::Concern
|
98
|
+
|
99
|
+
class_methods do
|
100
|
+
def find(*ids)
|
101
|
+
# Skip if model doesn't use prefixed ids
|
102
|
+
return super if _neat_id.blank?
|
103
|
+
|
104
|
+
neat_ids = ids.flatten.map do |id|
|
105
|
+
neat_id = _neat_id.decode(id, fallback: _neat_id_fallback)
|
106
|
+
raise Error, "#{id} is not a valid neat_id" if !_neat_id_fallback && neat_id.nil?
|
107
|
+
neat_id
|
108
|
+
end
|
109
|
+
neat_ids = [neat_ids] if ids.first.is_a?(Array)
|
110
|
+
|
111
|
+
super(*neat_ids)
|
112
|
+
end
|
113
|
+
|
114
|
+
def relation
|
115
|
+
super.tap { |r| r.extend ClassMethods }
|
116
|
+
end
|
117
|
+
|
118
|
+
def has_many(*args, &block)
|
119
|
+
options = args.extract_options!
|
120
|
+
options[:extend] = Array(options[:extend]).push(ClassMethods)
|
121
|
+
super(*args, **options, &block)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
module ToParam
|
127
|
+
extend ActiveSupport::Concern
|
128
|
+
|
129
|
+
def to_param
|
130
|
+
_neat_id.encode(id)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: neat_ids
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brad Thompson
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rails
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: 6.0.0
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 6.0.0
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: sqids
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.2.0
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 0.2.0
|
40
|
+
description: Neat IDs generates IDs with friendly prefixes for your models
|
41
|
+
email:
|
42
|
+
- bkt@brad-thompson.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- MIT-LICENSE
|
48
|
+
- README.md
|
49
|
+
- Rakefile
|
50
|
+
- lib/neat_ids.rb
|
51
|
+
- lib/neat_ids/engine.rb
|
52
|
+
- lib/neat_ids/neat_id.rb
|
53
|
+
- lib/neat_ids/version.rb
|
54
|
+
homepage: https://github.com/braddoeswebdev/neat_ids
|
55
|
+
licenses:
|
56
|
+
- MIT
|
57
|
+
metadata:
|
58
|
+
homepage_uri: https://github.com/braddoeswebdev/neat_ids
|
59
|
+
source_code_uri: https://github.com/braddoeswebdev/neat_ids
|
60
|
+
changelog_uri: https://github.com/braddoeswebdev/neat_ids
|
61
|
+
rdoc_options: []
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
requirements: []
|
75
|
+
rubygems_version: 3.6.7
|
76
|
+
specification_version: 4
|
77
|
+
summary: Neat IDs generates IDs with friendly prefixes for your models
|
78
|
+
test_files: []
|