Skip to content

Commit 5f461e0

Browse files
committed
Initial project files.
0 parents  commit 5f461e0

File tree

15 files changed

+558
-0
lines changed

15 files changed

+558
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Change Log
2+
3+
## v1.0.0
4+
- Initial release.

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
source 'https://rubygems.org'
2+
ruby '2.4.2'

Gemfile.lock

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
GEM
2+
remote: https://rubygems.org/
3+
specs:
4+
5+
PLATFORMS
6+
ruby
7+
8+
DEPENDENCIES
9+
10+
RUBY VERSION
11+
ruby 2.4.2p198
12+
13+
BUNDLED WITH
14+
1.16.2

MIT-LICENSE.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Copyright 2018 Brightcommerce, Inc. All rights reserved.
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.

README.md

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
# AttrSequence
2+
3+
AttrSequence is an ActiveRecord concern that generates scoped sequential numbers for models. This gem provides an `attr_sequence` macro that automatically assigns a unique, sequential number to each record. The sequential number is not a replacement for the database primary key, but rather adds another way to retrieve the object without exposing the primary key.
4+
5+
AttrSequence has been extracted from the Brightcommerce platform and is now used in multiple other software projects.
6+
7+
## Installation
8+
9+
To install add the line to your `Gemfile`:
10+
11+
``` ruby
12+
gem 'attr_sequence'
13+
```
14+
15+
And run `bundle install`.
16+
17+
The following configuration defaults are used by AttrSequence:
18+
19+
``` ruby
20+
AttrSequence.configure do |config|
21+
config.column = :number
22+
config.start_at = 1
23+
end
24+
```
25+
26+
You can override them by generating an initializer using the following command:
27+
28+
``` bash
29+
rails generate attr_sequence:initializer
30+
```
31+
32+
This will generate an initializer file in your project's `config/initializers` called `attr_sequence.rb` directory.
33+
34+
## Usage
35+
36+
It's generally a bad practice to expose your primary keys to the world in your URLs. However, it is often appropriate to number objects in sequence (in the context of a parent object).
37+
38+
For example, given a Question model that has many Answers, it makes sense to number answers sequentially for each individual question. You can achieve this with AttrSequence:
39+
40+
``` ruby
41+
class Question < ActiveRecord::Base
42+
has_many :answers
43+
end
44+
45+
class Answer < ActiveRecord::Base
46+
include AttrSequence
47+
belongs_to :question
48+
attr_sequence scope: :question_id
49+
end
50+
```
51+
52+
To autoload AttrSequence for all models, add the following to an initializer:
53+
54+
``` ruby
55+
require 'attr_sequence/active_record'
56+
```
57+
58+
You then don't need to `include AttrSequence` in any model.
59+
60+
To add a sequential number to a model, first add an integer column called `:number` to the model (or you many name the column anything you like and override the default). For example:
61+
62+
``` bash
63+
rails generate migration add_number_to_answers number:integer
64+
rake db:migrate
65+
```
66+
67+
Then, include the concern module and call the `attr_sequence` macro in your model class:
68+
69+
``` ruby
70+
class Answer < ActiveRecord::Base
71+
include AttrSequence
72+
belongs_to :question
73+
attr_sequence scope: :question_id
74+
end
75+
```
76+
77+
The scope option can be any attribute, but will typically be the foreign key of an associated parent object. You can even scope by multiple columns for polymorphic relationships:
78+
79+
``` ruby
80+
class Answer < ActiveRecord::Base
81+
include AttrSequence
82+
belongs_to :questionable, polymorphic: true
83+
attr_sequence scope: [:questionable_id, :questionable_type]
84+
end
85+
```
86+
87+
Multiple sequences can be defined by using the macro multiple times:
88+
89+
``` ruby
90+
class Answer < ActiveRecord::Base
91+
include AttrSequence
92+
belongs_to :account
93+
belongs_to :question
94+
95+
attr_sequence column: :question_answer_number, scope: :question_id
96+
attr_sequence column: :account_answer_number, scope: :account_id
97+
end
98+
```
99+
100+
## Schema and data integrity
101+
102+
*This gem is only concurrent-safe for PostgreSQL databases.* For other database systems, unexpected behavior may occur if you attempt to create records concurrently.
103+
104+
You can mitigate this somewhat by applying a unique index to your sequential number column (or a multicolumn unique index on sequential number and scope columns, if you are using scopes). This will ensure that you can never have duplicate sequential numbers within a scope, causing concurrent updates to instead raise a uniqueness error at the database-level.
105+
106+
It is also a good idea to apply a not-null constraint to your sequential number column as well if you never intend to skip it.
107+
108+
Here is an example migration for an `Answer` model that has a `:number` scoped to a `Question`:
109+
110+
``` ruby
111+
# app/db/migrations/20180101000000_create_answers.rb
112+
class CreateAnswers < ActiveRecord::Migration
113+
def change
114+
create_table :answers do |table|
115+
table.references :question
116+
table.column :number, :integer, null: false
117+
table.index [:number, :question_id], unique: true
118+
end
119+
end
120+
end
121+
```
122+
123+
## Configuration
124+
125+
### Overriding the default sequential ID column
126+
127+
By default, AttrSequence uses the `number` column and assumes it already exists. If you wish to store the sequential number in different integer column, simply specify the column name with the `:column` option:
128+
129+
``` ruby
130+
attr_sequence scope: :question_id, column: :my_sequential_id
131+
```
132+
133+
### Starting the sequence at a specific number
134+
135+
By default, AttrSequence begins sequences with 1. To start at a different integer, simply set the `start_at` option:
136+
137+
``` ruby
138+
attr_sequence start_at: 1000
139+
```
140+
141+
You may also pass a lambda to the `start_at` option:
142+
143+
``` ruby
144+
attr_sequence start_at: lambda { |r| r.computed_start_value }
145+
```
146+
147+
### Indexing the sequential number column
148+
149+
For optimal performance, it's a good idea to index the sequential number column on sequenced models.
150+
151+
### Skipping sequential ID generation
152+
153+
If you'd like to skip generating a sequential number under certain conditions, you may pass a lambda to the `skip` option:
154+
155+
``` ruby
156+
attr_sequence skip: lambda { |r| r.score == 0 }
157+
```
158+
159+
## Example
160+
161+
Suppose you have a question model that has many answers. This example demonstrates how to use AttrSequence to enable access to the nested answer resource via its sequential number.
162+
163+
``` ruby
164+
# app/models/question.rb
165+
class Question < ActiveRecord::Base
166+
has_many :answers
167+
end
168+
169+
# app/models/answer.rb
170+
class Answer < ActiveRecord::Base
171+
include AttrSequence
172+
belongs_to :question
173+
attr_sequence scope: :question_id
174+
175+
# Automatically use the sequential number in URLs
176+
def to_param
177+
self.number.to_s
178+
end
179+
end
180+
181+
# config/routes.rb
182+
resources :questions do
183+
resources :answers
184+
end
185+
186+
# app/controllers/answers_controller.rb
187+
class AnswersController < ApplicationController
188+
def show
189+
@question = Question.find(params[:question_id])
190+
@answer = @question.answers.find_by(number: params[:id])
191+
end
192+
end
193+
```
194+
195+
Now, answers are accessible via their sequential numbers:
196+
197+
```
198+
http://example.com/questions/5/answers/1 # Good
199+
```
200+
201+
instead of by their primary keys:
202+
203+
```
204+
http://example.com/questions/5/answer/32454 # Bad
205+
```
206+
207+
## Dependencies
208+
209+
AttrSequence gem has the following runtime dependencies:
210+
- activerecord >= 5.1.4
211+
- activesupport >= 5.1.4
212+
213+
## Compatibility
214+
215+
Tested with MRI 2.4.2 against Rails 5.2.2.
216+
217+
## Contributing
218+
219+
1. Fork it
220+
2. Create your feature branch (`git checkout -b my-new-feature`)
221+
3. Commit your changes (`git commit -am 'Add some feature'`)
222+
4. Push to the branch (`git push origin my-new-feature`)
223+
5. Create new Pull Request
224+
225+
## Credit
226+
227+
This gem was written and is maintained by [Jurgen Jocubeit](https://github.com/JurgenJocubeit), CEO and President Brightcommerce, Inc.
228+
229+
## License
230+
231+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
232+
233+
## Copyright
234+
235+
Copyright 2018 Brightcommerce, Inc.

Rakefile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
begin
2+
require 'bundler/setup'
3+
rescue LoadError
4+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5+
end
6+
7+
Bundler::GemHelper.install_tasks
8+
9+
require 'rake/testtask'
10+
11+
Rake::TestTask.new(:test) do |t|
12+
t.libs << 'lib'
13+
t.libs << 'test'
14+
t.pattern = 'test/**/*_test.rb'
15+
t.verbose = false
16+
end
17+
18+
task default: :test

attr_sequence.gemspec

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
require "./lib/attr_sequence/version"
2+
3+
Gem::Specification.new do |gem|
4+
gem.name = "attr_sequence"
5+
gem.version = AttrSequence::Version::Compact
6+
gem.summary = AttrSequence::Version::Summary
7+
gem.description = AttrSequence::Version::Description
8+
gem.authors = AttrSequence::Version::Author
9+
gem.email = AttrSequence::Version::Email
10+
gem.homepage = AttrSequence::Version::Homepage
11+
gem.license = AttrSequence::Version::License
12+
gem.metadata = AttrSequence::Version::Metadata
13+
gem.platform = Gem::Platform::RUBY
14+
15+
gem.required_ruby_version = '>= 2.3'
16+
gem.require_paths = ["lib"]
17+
gem.files = Dir[
18+
"{lib}/**/*",
19+
"MIT-LICENSE.md",
20+
"CHANGELOG.md",
21+
"README.md"
22+
]
23+
24+
gem.add_runtime_dependency 'activerecord', '>= 5.1.4'
25+
gem.add_runtime_dependency 'activesupport', '>= 5.1.4'
26+
end

lib/attr_sequence.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
require 'attr_sequence/attr_sequence'
2+
require 'attr_sequence/configuration'
3+
require 'attr_sequence/version'
4+
5+
module AttrSequence
6+
extend ActiveSupport::Autoload
7+
8+
@@configuration = nil
9+
10+
def self.configure
11+
@@configuration = Configuration.new
12+
yield(configuration) if block_given?
13+
configuration
14+
end
15+
16+
def self.configuration
17+
@@configuration || configure
18+
end
19+
20+
def self.method_missing(method_sym, *arguments, &block)
21+
if configuration.respond_to?(method_sym)
22+
configuration.send(method_sym)
23+
else
24+
super
25+
end
26+
end
27+
28+
def self.respond_to?(method_sym, include_private = false)
29+
if configuration.respond_to?(method_sym, include_private)
30+
true
31+
else
32+
super
33+
end
34+
end
35+
end

lib/attr_sequence/active_record.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ActiveRecord::Base.send :include, AttrSequence

0 commit comments

Comments
 (0)