attr_sequence 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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: []