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.
@@ -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
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -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'
@@ -0,0 +1 @@
1
+ ruby 2.7.1
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
@@ -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
@@ -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.
@@ -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).
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Identifiable
4
+ module Stylists
5
+ class Uuid
6
+ def random_id
7
+ SecureRandom.uuid
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Identifiable
4
+ VERSION = '0.1.0'
5
+ 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: []