identifiable 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 +7 -0
- data/.github/workflows/ci.yml +36 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +21 -0
- data/.tool-versions +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +77 -0
- data/LICENSE.txt +21 -0
- data/README.md +125 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/identifiable.gemspec +30 -0
- data/lib/identifiable.rb +17 -0
- data/lib/identifiable/errors.rb +63 -0
- data/lib/identifiable/model.rb +118 -0
- data/lib/identifiable/stylist.rb +28 -0
- data/lib/identifiable/stylists/alphanumeric.rb +15 -0
- data/lib/identifiable/stylists/numeric.rb +27 -0
- data/lib/identifiable/stylists/uuid.rb +11 -0
- data/lib/identifiable/version.rb +5 -0
- metadata +136 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 490257eea4bca942162766045db54e9cac0b3cdbedcea08a418e3b659cc209ec
|
4
|
+
data.tar.gz: e3cfa1004c351eac0170b1740e291b9f3a878c72a450b9cc7c787ae999b33d83
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e54d72207095067e3b7fea091a5bb41665525d43c5ececa51c228f5ee824d1fc7f55c5c2ca5aac177504456508b60bef7a7d61a27757cc3a801be269cc2b4f0c
|
7
|
+
data.tar.gz: e685fb908d6122fc2e4ce52cb0957bfb50c813705a9acecfe64ddb5ec7fea09dbbddacb346ec70d83de2c4b73107189edc53b5ae0554d58cafc68eed24189da3
|
@@ -0,0 +1,36 @@
|
|
1
|
+
name: CI
|
2
|
+
on: [push, pull_request]
|
3
|
+
jobs:
|
4
|
+
test:
|
5
|
+
name: 'Test Suite'
|
6
|
+
strategy:
|
7
|
+
matrix:
|
8
|
+
ruby: [2.5, 2.6, 2.7]
|
9
|
+
runs-on: ubuntu-latest
|
10
|
+
steps:
|
11
|
+
- uses: actions/checkout@v2
|
12
|
+
- uses: ruby/setup-ruby@v1
|
13
|
+
with:
|
14
|
+
ruby-version: ${{ matrix.ruby }}
|
15
|
+
- uses: actions/cache@v1
|
16
|
+
with:
|
17
|
+
path: vendor/bundle
|
18
|
+
key: bundle-${{ matrix.ruby }}-${{ hashFiles('**/*.gemspec') }}
|
19
|
+
restore-keys: bundle-${{ matrix.ruby }}
|
20
|
+
- run: bundle install --jobs 4 --retry 3 --path vendor/bundle
|
21
|
+
- run: bundle exec rake
|
22
|
+
lint:
|
23
|
+
name: 'Rubocop'
|
24
|
+
runs-on: ubuntu-latest
|
25
|
+
steps:
|
26
|
+
- uses: actions/checkout@v2
|
27
|
+
- uses: ruby/setup-ruby@v1
|
28
|
+
with:
|
29
|
+
ruby-version: 2.7.2
|
30
|
+
- uses: actions/cache@v1
|
31
|
+
with:
|
32
|
+
path: vendor/bundle
|
33
|
+
key: bundle-2.7.2-${{ hashFiles('**/*.gemspec') }}
|
34
|
+
restore-keys: bundle-2.7.2
|
35
|
+
- run: bundle install --jobs 4 --retry 3 --path vendor/bundle
|
36
|
+
- run: bundle exec rubocop
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
AllCops:
|
2
|
+
NewCops: enable
|
3
|
+
|
4
|
+
Style/Documentation:
|
5
|
+
Enabled: false
|
6
|
+
|
7
|
+
Layout/LineLength:
|
8
|
+
Enabled: false
|
9
|
+
|
10
|
+
Metrics/MethodLength:
|
11
|
+
Max: 20
|
12
|
+
|
13
|
+
Metrics/ClassLength:
|
14
|
+
Max: 1500
|
15
|
+
|
16
|
+
Metrics/BlockLength:
|
17
|
+
Max: 50
|
18
|
+
Exclude:
|
19
|
+
- 'Rakefile'
|
20
|
+
- '**/*.rake'
|
21
|
+
- 'spec/**/*.rb'
|
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 2.7.1
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at hi@tpritc.com. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [https://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: https://contributor-covenant.org
|
74
|
+
[version]: https://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
identifiable (0.1.0)
|
5
|
+
activerecord (> 4.2)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activemodel (6.0.3.4)
|
11
|
+
activesupport (= 6.0.3.4)
|
12
|
+
activerecord (6.0.3.4)
|
13
|
+
activemodel (= 6.0.3.4)
|
14
|
+
activesupport (= 6.0.3.4)
|
15
|
+
activesupport (6.0.3.4)
|
16
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
17
|
+
i18n (>= 0.7, < 2)
|
18
|
+
minitest (~> 5.1)
|
19
|
+
tzinfo (~> 1.1)
|
20
|
+
zeitwerk (~> 2.2, >= 2.2.2)
|
21
|
+
ast (2.4.1)
|
22
|
+
concurrent-ruby (1.1.7)
|
23
|
+
diff-lcs (1.4.4)
|
24
|
+
i18n (1.8.5)
|
25
|
+
concurrent-ruby (~> 1.0)
|
26
|
+
minitest (5.14.2)
|
27
|
+
parallel (1.20.0)
|
28
|
+
parser (2.7.2.0)
|
29
|
+
ast (~> 2.4.1)
|
30
|
+
rainbow (3.0.0)
|
31
|
+
rake (12.3.3)
|
32
|
+
regexp_parser (1.8.2)
|
33
|
+
rexml (3.2.4)
|
34
|
+
rspec (3.10.0)
|
35
|
+
rspec-core (~> 3.10.0)
|
36
|
+
rspec-expectations (~> 3.10.0)
|
37
|
+
rspec-mocks (~> 3.10.0)
|
38
|
+
rspec-core (3.10.0)
|
39
|
+
rspec-support (~> 3.10.0)
|
40
|
+
rspec-expectations (3.10.0)
|
41
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
42
|
+
rspec-support (~> 3.10.0)
|
43
|
+
rspec-mocks (3.10.0)
|
44
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
45
|
+
rspec-support (~> 3.10.0)
|
46
|
+
rspec-support (3.10.0)
|
47
|
+
rubocop (1.3.0)
|
48
|
+
parallel (~> 1.10)
|
49
|
+
parser (>= 2.7.1.5)
|
50
|
+
rainbow (>= 2.2.2, < 4.0)
|
51
|
+
regexp_parser (>= 1.8)
|
52
|
+
rexml
|
53
|
+
rubocop-ast (>= 1.1.1)
|
54
|
+
ruby-progressbar (~> 1.7)
|
55
|
+
unicode-display_width (>= 1.4.0, < 2.0)
|
56
|
+
rubocop-ast (1.1.1)
|
57
|
+
parser (>= 2.7.1.5)
|
58
|
+
ruby-progressbar (1.10.1)
|
59
|
+
sqlite3 (1.4.2)
|
60
|
+
thread_safe (0.3.6)
|
61
|
+
tzinfo (1.2.7)
|
62
|
+
thread_safe (~> 0.1)
|
63
|
+
unicode-display_width (1.7.0)
|
64
|
+
zeitwerk (2.4.0)
|
65
|
+
|
66
|
+
PLATFORMS
|
67
|
+
ruby
|
68
|
+
|
69
|
+
DEPENDENCIES
|
70
|
+
identifiable!
|
71
|
+
rake (~> 12.0)
|
72
|
+
rspec (~> 3.0)
|
73
|
+
rubocop (~> 1.3)
|
74
|
+
sqlite3
|
75
|
+
|
76
|
+
BUNDLED WITH
|
77
|
+
2.1.4
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Tom Pritchard
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
# Identifiable
|
2
|
+
|
3
|
+
Identifiable makes is really quick and easy way to add random, customizable, public-facing IDs to your models. These are great for URLs and reports, where you might not want to share the record's `id` attribute.
|
4
|
+
|
5
|
+
## Why do I need public-facing IDs?
|
6
|
+
|
7
|
+
If you're asking this question, you probably use your record's `id` attribute as its identifier in its URL. This can be an issue for two main reasons:
|
8
|
+
|
9
|
+
1. If you've got a low number of records (if you're just getting started, for instance) it might look unprofessional to have a low ID number. Simply put, `https://example.app/orders/14` doesn't look as good as `https://example.app/orders/87133275`. Even if you've only got 14 orders so far, you might want to _look_ like you've got more. Fake it 'til you make it!
|
10
|
+
2. If you're exposing things to the open web, using auto-incrementing IDs means someone hoping to scrape your app can just increment the ID parameter in the URL and get everything. With randomly generated public IDs, scrapers can't just add 1 to each ID to find valid pages.
|
11
|
+
|
12
|
+
Identifiable makes it really simple to generate and use random public-facing IDs in your Rails applications.
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
# Before:
|
16
|
+
orders_url(id: @order.id) # → https://example.app/orders/14
|
17
|
+
|
18
|
+
# After:
|
19
|
+
orders_url(id: @order.public_id) # → https://example.app/orders/87133275
|
20
|
+
```
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Add this line to your application's Gemfile:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
gem 'identifiable'
|
28
|
+
```
|
29
|
+
|
30
|
+
Then run `bundle install` and you'll be set.
|
31
|
+
|
32
|
+
## Usage
|
33
|
+
|
34
|
+
Adding Identifiable to your Rails models couldn't be simpler. The easiest, most default way to add public IDs is to just add `identifiable` to your model. This'll default to 8-character long numeric public IDs (like `23843274` or `98237268`, for instance) on the `public_id` column of your model. These public IDs will be automatically set when you create your records.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
class Order < ApplicationRecord
|
38
|
+
identifiable
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
Identifiable doesn't create the columns for your model, so you will need to add those yourself when you create your models (or add the columns when adding Identifiable to an existing model). These columns will also need an index on them. To add a `public_id` string column with an index to your `Order` model, you can run in your command line:
|
43
|
+
|
44
|
+
```bash
|
45
|
+
bundle exec rails generate migration add_public_id_to_orders public_id:index
|
46
|
+
bundle exec rails db:migrate
|
47
|
+
```
|
48
|
+
|
49
|
+
If you don't have the public id column set up, Identifiable will raise an error.
|
50
|
+
|
51
|
+
### Finding by your model's public ID
|
52
|
+
|
53
|
+
If you're used to using `Model.find(id)` in your controllers, no need to fret! Now you can replace it with `Model.find_by_public_id!(public_id)` and you'll get the same behavior, including raising `ActiveRecord::RecordNotFound` if the record doesn't exist. It's plug-and-play.
|
54
|
+
|
55
|
+
You can also use `Model.find_by_public_id(public_id)`, which is similar, but does not raise an error if the record doesn't exist, you'll just get `nil` instead.
|
56
|
+
|
57
|
+
Because your public ID is just a column in your database, you can also use all the standard Rails ways of accessing records, including `#where` and `#find_by`.
|
58
|
+
|
59
|
+
### Adding public IDs to existing records
|
60
|
+
|
61
|
+
Public IDs are assigned when saving a record if the public ID isn't already set, so to add public IDs to existing records, you just need to re-run a `#save` on each one, like so:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
Order.all.find_each do |order|
|
65
|
+
order.save!
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
## Customization
|
70
|
+
|
71
|
+
While Identifiable strives to have useful defaults, you may want to customize your public IDs for your particular application, or even for particular models in your application.
|
72
|
+
|
73
|
+
### Different public ID styles
|
74
|
+
|
75
|
+
By default, Identifiable will generate numeric public IDs, with each character between 0 and 9, but it can also generate alphanumeric public IDs and UUID public ids. To chose your public ID style, simply pass it in as a parameter to `identifiable` on your model.
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
class Order < ApplicationRecord
|
79
|
+
# Valid options for style are :numeric (default), :alphanumeric, and :uuid
|
80
|
+
identifiable style: :alphanumeric
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
Here's how each available style looks:
|
85
|
+
|
86
|
+
* `:numeric` produces public ids with numbers between 0 and 9 for each character, and looks like `23843274`.
|
87
|
+
* `:alphanumeric` produces public ids with uppercase letters (A-Z), lowercase letters (a-z), and numbers (0-9) for each character, and looks like `a3ZG8fkP`.
|
88
|
+
* `:uuid` produces public ids that match the [UUID specification in RFC 4122](https://tools.ietf.org/html/rfc4122), and look like `adea0700-94de-4075-bbf0-7853cffcca50`.
|
89
|
+
|
90
|
+
### Changing the public ID column
|
91
|
+
|
92
|
+
By default, Identifiable will assume your public ID column is `public_id`, but you can pick another column if you want. For instance, you might want to use `share_link_id` for a publicly shareable link. To do so, simply pass the column name as a symbol to `identifiable` on your model.
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
class Order < ApplicationRecord
|
96
|
+
# Defaults to `:public_id`. In this example, the column `share_link_id` must
|
97
|
+
# exist in the `orders` table, otherwise Identifiable will raise an error.
|
98
|
+
identifiable column: :share_link_id
|
99
|
+
end
|
100
|
+
```
|
101
|
+
|
102
|
+
### Changing the length of the public ID
|
103
|
+
|
104
|
+
Numeric and alphanumeric public IDs default to 8 characters long, but in your application you might want more (or less) than that. The `length` parameter of `identifiable` is used to set the length of these public IDs, and defaults to `8`.
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
class Order < ApplicationRecord
|
108
|
+
# This configuration will produce 16 character long alphanumeric public IDs.
|
109
|
+
identifiable style: :alphanumeric, length: 16
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
The `length` parameter is ignored if you're using `style: :uuid`, because UUIDs already have a fixed length. The `length` parameter also needs be an Integer, and can't be less than `4` or greater than `128`. If any of these constraints are broken, Identifiable will raise an error letting you know.
|
114
|
+
|
115
|
+
## Contributing
|
116
|
+
|
117
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/tpritc/identifiable). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/tpritc/identifiable/blob/main/CODE_OF_CONDUCT.md).
|
118
|
+
|
119
|
+
## License
|
120
|
+
|
121
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). If you or your organization need a custom, commercial license for any reason, [send me an email](mailto:hi@tpritc.com) and I'll be happy to set something up for you.
|
122
|
+
|
123
|
+
## Code of Conduct
|
124
|
+
|
125
|
+
Everyone interacting in the Identifiable project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/tpritc/identifiable/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'identifiable'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/identifiable/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.version = Identifiable::VERSION
|
7
|
+
spec.name = 'identifiable'
|
8
|
+
spec.authors = ['Tom Pritchard']
|
9
|
+
spec.email = ['hi@tpritc.com']
|
10
|
+
|
11
|
+
spec.summary = 'A quick and easy way to add random, customizable, public-facing IDs to your models.'
|
12
|
+
spec.homepage = 'https://github.com/tpritc/identifiable'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
spec.required_ruby_version = '>= 2.5.0'
|
15
|
+
|
16
|
+
# Specify which files should be added to the gem when it is released.
|
17
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
18
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
19
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
20
|
+
end
|
21
|
+
spec.bindir = 'exe'
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ['lib']
|
24
|
+
|
25
|
+
spec.add_dependency 'activerecord', '> 4.2'
|
26
|
+
spec.add_development_dependency 'rake', '~> 12.0'
|
27
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
28
|
+
spec.add_development_dependency 'rubocop', '~> 1.3'
|
29
|
+
spec.add_development_dependency 'sqlite3'
|
30
|
+
end
|
data/lib/identifiable.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Identifiable
|
4
|
+
module Stylists
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'active_record'
|
9
|
+
require 'identifiable/stylist'
|
10
|
+
require 'identifiable/stylists/alphanumeric'
|
11
|
+
require 'identifiable/stylists/numeric'
|
12
|
+
require 'identifiable/stylists/uuid'
|
13
|
+
require 'identifiable/errors'
|
14
|
+
require 'identifiable/model'
|
15
|
+
require 'identifiable/version'
|
16
|
+
|
17
|
+
ActiveRecord::Base.include Identifiable::Model
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Identifiable
|
4
|
+
module Errors
|
5
|
+
class ColumnMustBeASymbolError < StandardError
|
6
|
+
def initialize(column_value)
|
7
|
+
column_value_string = column_value.to_s || 'nil'
|
8
|
+
column_value_class = column_value.class.to_s
|
9
|
+
super("The identifiable's column must be a symbol. You passed in #{column_value_string}, which was a #{column_value_class}.")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class ColumnCannotBeIdError < StandardError
|
14
|
+
def initialize
|
15
|
+
super('The identifiable\'s column cannot be :id, since that\'s your primary key. You should create another index column in your table with a name like :public_id.')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class ColumnMustExistInTheTableError < StandardError
|
20
|
+
def initialize(column_value, valid_columns:)
|
21
|
+
column_value_string = column_value.to_s || 'nil'
|
22
|
+
super("The identifiable's column must exist on the table for this model. You passed in #{column_value_string}, but the valid columns are: #{valid_columns}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class StyleMustBeAValidStyleError < StandardError
|
27
|
+
def initialize(style_value)
|
28
|
+
style_value_string = style_value.to_s || 'nil'
|
29
|
+
super("The identifiable\'s style must be a valid stylist. You passed in #{style_value_string}, but the valid options are: #{Identifiable::Stylist::VALID_STYLES}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class LengthMustBeAnIntegerError < StandardError
|
34
|
+
def initialize
|
35
|
+
super('The identifiable\'s length must be an Integer.')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class LengthIsTooShortError < StandardError
|
40
|
+
def initialize
|
41
|
+
super('The identifiable\'s length must be at least 4.')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class LengthIsTooLongError < StandardError
|
46
|
+
def initialize
|
47
|
+
super('The identifiable\'s length cannot be more than 128.')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class LengthMustBeNilIfStyleIsUuidError < StandardError
|
52
|
+
def initialize
|
53
|
+
super('The identifiable\'s length cannot be set if you\'re using the :uuid style, because UUIDs have a fixed length. You should remove the length parameter.')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class RanOutOfAttemptsToSetPublicIdError < StandardError
|
58
|
+
def initialize
|
59
|
+
super('We tried 100 times to find an unused public ID, but we could not find one. You should increase the length parameter of the public ID.')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Identifiable
|
4
|
+
module Model
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
class_methods do
|
8
|
+
attr_reader :identifiable_column
|
9
|
+
attr_reader :identifiable_style
|
10
|
+
attr_reader :identifiable_length
|
11
|
+
|
12
|
+
def identifiable(column: :public_id, style: :numeric, length: 8)
|
13
|
+
@identifiable_column = column
|
14
|
+
@identifiable_style = style
|
15
|
+
@identifiable_length = length
|
16
|
+
|
17
|
+
_identifiable_validate_column_must_be_a_symbol
|
18
|
+
_identifiable_validate_column_cannot_be_id
|
19
|
+
_identifiable_validate_column_must_be_in_the_table
|
20
|
+
_identifiable_validate_style_must_be_a_valid_style
|
21
|
+
_identifiable_validate_length_must_be_an_integer
|
22
|
+
_identifiable_validate_length_must_be_in_a_valid_range
|
23
|
+
|
24
|
+
before_create :set_public_id!
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_by_public_id(public_id)
|
28
|
+
where(Hash[identifiable_column, public_id]).first
|
29
|
+
end
|
30
|
+
|
31
|
+
def find_by_public_id!(public_id)
|
32
|
+
result = find_by_public_id(public_id)
|
33
|
+
raise ActiveRecord::RecordNotFound unless result.present?
|
34
|
+
|
35
|
+
result
|
36
|
+
end
|
37
|
+
|
38
|
+
# We only accept symbols for the column parameter, so we need to raise an
|
39
|
+
# error if anything other than a symbol is passed in.
|
40
|
+
def _identifiable_validate_column_must_be_a_symbol
|
41
|
+
return if @identifiable_column.is_a? Symbol
|
42
|
+
|
43
|
+
raise Identifiable::Errors::ColumnMustBeASymbolError, @identifiable_column
|
44
|
+
end
|
45
|
+
|
46
|
+
# The column parameter cannot be :id, since that's reserved for the
|
47
|
+
# primary key, so raise an error if the column parameter is :id.
|
48
|
+
def _identifiable_validate_column_cannot_be_id
|
49
|
+
return unless @identifiable_column == :id
|
50
|
+
|
51
|
+
raise Identifiable::Errors::ColumnCannotBeIdError
|
52
|
+
end
|
53
|
+
|
54
|
+
# The column parameter must be in the model's table, so check that the
|
55
|
+
# column corresponds to a column in the model's table, and raise an error
|
56
|
+
# if it is not.
|
57
|
+
def _identifiable_validate_column_must_be_in_the_table
|
58
|
+
return if column_names.include? @identifiable_column.to_s
|
59
|
+
|
60
|
+
raise Identifiable::Errors::ColumnMustExistInTheTableError.new(@identifiable_column, valid_columns: column_names)
|
61
|
+
end
|
62
|
+
|
63
|
+
# We can only use valid styles, so check that the style parameter is a
|
64
|
+
# valid style, and raise an error if it is not.
|
65
|
+
def _identifiable_validate_style_must_be_a_valid_style
|
66
|
+
return if Identifiable::Stylist::VALID_STYLES.include? @identifiable_style
|
67
|
+
|
68
|
+
raise Identifiable::Errors::StyleMustBeAValidStyleError, @identifiable_style
|
69
|
+
end
|
70
|
+
|
71
|
+
# The length parameter must be an Integer, so if it's something else then
|
72
|
+
# raise an error.
|
73
|
+
def _identifiable_validate_length_must_be_an_integer
|
74
|
+
return if @identifiable_length.is_a? Integer
|
75
|
+
|
76
|
+
raise Identifiable::Errors::LengthMustBeAnIntegerError
|
77
|
+
end
|
78
|
+
|
79
|
+
# The length parameter can only be within a certain range to prevent
|
80
|
+
# public IDs that are too short or too long, so raise an error if the
|
81
|
+
# length is too short or too long.
|
82
|
+
def _identifiable_validate_length_must_be_in_a_valid_range
|
83
|
+
raise Identifiable::Errors::LengthIsTooShortError if @identifiable_length < 4
|
84
|
+
raise Identifiable::Errors::LengthIsTooLongError if @identifiable_length > 128
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# If we don't have a public ID yet, this method fetches the stylist for
|
89
|
+
# this class and finds a new, valid public id, and assigns it to the public
|
90
|
+
# ID column.
|
91
|
+
def set_public_id!
|
92
|
+
# We don't want to set the public id if there's already a value in the
|
93
|
+
# column, so exit early if that's the case.
|
94
|
+
return unless self[self.class.identifiable_column].blank?
|
95
|
+
|
96
|
+
# Create a new stylist for this record, that'll return random IDs
|
97
|
+
# matching the class' style and length options.
|
98
|
+
stylist = Identifiable::Stylist.new(record: self)
|
99
|
+
new_public_id = nil
|
100
|
+
|
101
|
+
# Loop until we find an unused public ID or we run out of attempts.
|
102
|
+
100.times do
|
103
|
+
new_public_id = stylist.random_id
|
104
|
+
break if self.class.find_by_public_id(new_public_id).nil?
|
105
|
+
|
106
|
+
new_public_id = nil
|
107
|
+
end
|
108
|
+
|
109
|
+
# If we ran out of attempts, this probably means the length is too short,
|
110
|
+
# since we kept colliding with existing records. Raise an error to let
|
111
|
+
# the developers know that they need to up the length of the public ID.
|
112
|
+
raise Identifiable::Errors::RanOutOfAttemptsToSetPublicIdError if new_public_id.nil?
|
113
|
+
|
114
|
+
# If we got this far, we've got a new valid public ID, time to set it!
|
115
|
+
self[self.class.identifiable_column] = new_public_id
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Identifiable
|
4
|
+
class Stylist
|
5
|
+
VALID_STYLES = %i[numeric alphanumeric uuid].freeze
|
6
|
+
|
7
|
+
def initialize(record:)
|
8
|
+
@record = record
|
9
|
+
@stylist = stylist
|
10
|
+
end
|
11
|
+
|
12
|
+
def random_id
|
13
|
+
@stylist.random_id
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def stylist
|
19
|
+
case @record.class.identifiable_style
|
20
|
+
when :numeric then Identifiable::Stylists::Numeric.new(record: @record)
|
21
|
+
when :alphanumeric then Identifiable::Stylists::Alphanumeric.new(record: @record)
|
22
|
+
when :uuid then Identifiable::Stylists::Uuid.new
|
23
|
+
else
|
24
|
+
raise Identifiable::Errors::StyleMustBeAValidStyle
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Identifiable
|
4
|
+
module Stylists
|
5
|
+
class Alphanumeric
|
6
|
+
def initialize(record:)
|
7
|
+
@record = record
|
8
|
+
end
|
9
|
+
|
10
|
+
def random_id
|
11
|
+
SecureRandom.alphanumeric(@record.class.identifiable_length)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Identifiable
|
4
|
+
module Stylists
|
5
|
+
class Numeric
|
6
|
+
def initialize(record:)
|
7
|
+
@record = record
|
8
|
+
@scale = scale
|
9
|
+
end
|
10
|
+
|
11
|
+
def random_id
|
12
|
+
(SecureRandom.random_number(@scale.max - @scale.min) + @scale.min).to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def scale
|
18
|
+
length = @record.class.identifiable_length
|
19
|
+
|
20
|
+
minimum = 10**(length - 1)
|
21
|
+
maximum = (10**length) - 1
|
22
|
+
|
23
|
+
minimum..maximum
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: identifiable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tom Pritchard
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-11-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '12.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '12.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.3'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.3'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- hi@tpritc.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".github/workflows/ci.yml"
|
91
|
+
- ".gitignore"
|
92
|
+
- ".rspec"
|
93
|
+
- ".rubocop.yml"
|
94
|
+
- ".tool-versions"
|
95
|
+
- CODE_OF_CONDUCT.md
|
96
|
+
- Gemfile
|
97
|
+
- Gemfile.lock
|
98
|
+
- LICENSE.txt
|
99
|
+
- README.md
|
100
|
+
- Rakefile
|
101
|
+
- bin/console
|
102
|
+
- bin/setup
|
103
|
+
- identifiable.gemspec
|
104
|
+
- lib/identifiable.rb
|
105
|
+
- lib/identifiable/errors.rb
|
106
|
+
- lib/identifiable/model.rb
|
107
|
+
- lib/identifiable/stylist.rb
|
108
|
+
- lib/identifiable/stylists/alphanumeric.rb
|
109
|
+
- lib/identifiable/stylists/numeric.rb
|
110
|
+
- lib/identifiable/stylists/uuid.rb
|
111
|
+
- lib/identifiable/version.rb
|
112
|
+
homepage: https://github.com/tpritc/identifiable
|
113
|
+
licenses:
|
114
|
+
- MIT
|
115
|
+
metadata: {}
|
116
|
+
post_install_message:
|
117
|
+
rdoc_options: []
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 2.5.0
|
125
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - ">="
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
requirements: []
|
131
|
+
rubygems_version: 3.1.2
|
132
|
+
signing_key:
|
133
|
+
specification_version: 4
|
134
|
+
summary: A quick and easy way to add random, customizable, public-facing IDs to your
|
135
|
+
models.
|
136
|
+
test_files: []
|