attr_sequence 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5dd41bbcae7b5ff5580d666c830057a8e2d78506
4
+ data.tar.gz: cd9e7655afcc288c665ba4217749f8d875b52d84
5
+ SHA512:
6
+ metadata.gz: ac2121044636d2bbf40bc85869d1e65e3e48cda41f144e5d5656cbbd838618c70331f68434318811b8e5885c52c3ca324ee72c4f06aa686c3bc3a6f132ef7ece
7
+ data.tar.gz: 2be94b1269c7a4c3a3d3caee6f1b640525752ca25182fd56e406ba5ef976f03bc6a941357c4257b4bc3d893ae8c128ac49c0c534786cc5ceea8006d02ad6dac8
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ # Change Log
2
+
3
+ ## v1.0.0
4
+ - Initial release.
data/MIT-LICENSE.md ADDED
@@ -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.
data/README.md ADDED
@@ -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.
@@ -0,0 +1 @@
1
+ ActiveRecord::Base.send :include, AttrSequence
@@ -0,0 +1,68 @@
1
+ require_relative './generator'
2
+ require 'active_support/concern'
3
+ require 'active_support/core_ext/hash/slice'
4
+ require 'active_support/core_ext/class/attribute_accessors'
5
+
6
+ module AttrSequence
7
+ extend ActiveSupport::Concern
8
+
9
+ SequenceColumnExists = Class.new(StandardError)
10
+
11
+ class_methods do
12
+ # Public: Defines ActiveRecord callbacks to set a sequential number scoped
13
+ # on a specific class.
14
+ #
15
+ # Can be called multiple times to add hooks for different column names.
16
+ #
17
+ # options - The Hash of options for configuration:
18
+ # :scope - The Symbol representing the columm on which the
19
+ # number should be scoped (default: nil)
20
+ # :column - The Symbol representing the column that stores the
21
+ # number (default: :number)
22
+ # :start_at - The Integer value at which the sequence should
23
+ # start (default: 1)
24
+ # :skip - Skips the number generation when the lambda
25
+ # expression evaluates to nil. Gets passed the
26
+ # model object
27
+ #
28
+ # Examples
29
+ #
30
+ # class Answer < ActiveRecord::Base
31
+ # include AttrSequence
32
+ # belongs_to :question
33
+ # attr_sequence scope: :question_id
34
+ # end
35
+ #
36
+ # Returns nothing.
37
+ def attr_sequence(options = {})
38
+ unless defined?(sequence_options)
39
+ mattr_accessor :sequence_options, instance_accessor: false
40
+ self.sequence_options = []
41
+
42
+ before_save :set_numbers
43
+ end
44
+
45
+ default_options = {column: AttrSequence.column, start_at: AttrSequence.start_at}
46
+ options = default_options.merge(options)
47
+ column_name = options[:column]
48
+
49
+ if sequence_options.any? {|options| options[:column] == column_name}
50
+ raise(SequenceColumnExists, <<-MSG.squish)
51
+ Tried to set #{column_name} as a sequence but there was already a
52
+ definition here. Did you accidentally call attr_sequence multiple
53
+ times on the same column?
54
+ MSG
55
+ else
56
+ sequence_options << options
57
+ end
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def set_numbers
64
+ self.class.base_class.sequence_options.each do |options|
65
+ AttrSequence::Generator.new(self, options).set
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,19 @@
1
+ module AttrSequence
2
+ class Configuration
3
+ def column
4
+ @column ||= :number
5
+ end
6
+
7
+ def column=(value)
8
+ @column = value
9
+ end
10
+
11
+ def start_at
12
+ @start_at ||= 1
13
+ end
14
+
15
+ def start_at=(value)
16
+ @start_at = value
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,82 @@
1
+ module AttrSequence
2
+ class Generator
3
+ attr_reader :record, :scope, :column, :start_at, :skip
4
+
5
+ def initialize(record, options = {})
6
+ @record = record
7
+ @scope = options[:scope]
8
+ @column = options[:column].to_sym
9
+ @start_at = options[:start_at]
10
+ @skip = options[:skip]
11
+ end
12
+
13
+ def set
14
+ return if number_set? || skip?
15
+ lock_table
16
+ record.send(:"#{column}=", next_number)
17
+ end
18
+
19
+ def number_set?
20
+ !record.send(column).nil?
21
+ end
22
+
23
+ def skip?
24
+ skip && skip.call(record)
25
+ end
26
+
27
+ def next_number
28
+ next_number_in_sequence.tap do |number|
29
+ number += 1 until unique?(number)
30
+ end
31
+ end
32
+
33
+ def next_number_in_sequence
34
+ start_at = self.start_at.respond_to?(:call) ? self.start_at.call(record) : self.start_at
35
+ return start_at unless last_record = find_last_record
36
+ max(last_record.send(column) + 1, start_at)
37
+ end
38
+
39
+ def unique?(number)
40
+ build_scope(*scope) do
41
+ rel = base_relation
42
+ rel = rel.where("NOT number = ?", record.number) if record.persisted?
43
+ rel.where(column => number)
44
+ end.count == 0
45
+ end
46
+
47
+ private
48
+
49
+ def lock_table
50
+ if postgresql?
51
+ record.class.connection.execute("LOCK TABLE #{record.class.table_name} IN EXCLUSIVE MODE")
52
+ end
53
+ end
54
+
55
+ def postgresql?
56
+ defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) &&
57
+ record.class.connection.instance_of?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
58
+ end
59
+
60
+ def base_relation
61
+ record.class.base_class.unscoped
62
+ end
63
+
64
+ def find_last_record
65
+ build_scope(*scope) do
66
+ base_relation.
67
+ where("#{column.to_s} IS NOT NULL").
68
+ order("#{column.to_s} DESC")
69
+ end.first
70
+ end
71
+
72
+ def build_scope(*columns)
73
+ rel = yield
74
+ columns.each { |c| rel = rel.where(c => record.send(c.to_sym)) }
75
+ rel
76
+ end
77
+
78
+ def max(*values)
79
+ values.to_a.max
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,16 @@
1
+ module AttrSequence
2
+ module Version
3
+ Major = 1
4
+ Minor = 0
5
+ Revision = 0
6
+ Prerelease = nil
7
+ Compact = [Major, Minor, Revision, Prerelease].compact.join('.')
8
+ Summary = "AttrSequence v#{Compact}"
9
+ Description = "An ActiveRecord concern that generates scoped sequential IDs for models."
10
+ Author = "Jurgen Jocubeit"
11
+ Email = "support@brightcommerce.com"
12
+ Homepage = "https://github.com/brightcommerce/attr_sequence"
13
+ Metadata = {'copyright' => 'Copyright 2018 Brightcommerce, Inc. All Rights Reserved.'}
14
+ License = "MIT"
15
+ end
16
+ end
@@ -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
@@ -0,0 +1,14 @@
1
+ require 'rails/generators'
2
+
3
+ module AttrSequence
4
+ class InitializerGenerator < ::Rails::Generators::Base
5
+
6
+ namespace "attr_sequence:initializer"
7
+ source_root File.join(File.dirname(__FILE__), 'templates')
8
+
9
+ def create_initializer_file
10
+ template 'initializer.rb', 'config/initializers/attr_sequence.rb'
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,4 @@
1
+ AttrSequence.configure do |config|
2
+ # config.column = :number
3
+ # config.start_at = 1
4
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: attr_sequence
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jurgen Jocubeit
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-12-24 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: 5.1.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 5.1.4
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 5.1.4
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 5.1.4
41
+ description: An ActiveRecord concern that generates scoped sequential IDs for models.
42
+ email: support@brightcommerce.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - CHANGELOG.md
48
+ - MIT-LICENSE.md
49
+ - README.md
50
+ - lib/attr_sequence.rb
51
+ - lib/attr_sequence/active_record.rb
52
+ - lib/attr_sequence/attr_sequence.rb
53
+ - lib/attr_sequence/configuration.rb
54
+ - lib/attr_sequence/generator.rb
55
+ - lib/attr_sequence/version.rb
56
+ - lib/generators/attr_sequence/initializer/initializer_generator.rb
57
+ - lib/generators/attr_sequence/initializer/templates/initializer.rb
58
+ homepage: https://github.com/brightcommerce/attr_sequence
59
+ licenses:
60
+ - MIT
61
+ metadata:
62
+ copyright: Copyright 2018 Brightcommerce, Inc. All Rights Reserved.
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '2.3'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubyforge_project:
79
+ rubygems_version: 2.6.13
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: AttrSequence v1.0.0
83
+ test_files: []