activerecord-rescue_from_duplicate 0.0.6 → 0.0.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 177b4e3d186607ace00720b9a9aa0ef36c4544ba
4
- data.tar.gz: d1572e760fa9e8db750096b2ccf0ff8ce8103951
3
+ metadata.gz: 95ffd1c815f0296026de14cf92106766488a5472
4
+ data.tar.gz: 2f54109b6dbb3330942a64ce0644f95529e2431a
5
5
  SHA512:
6
- metadata.gz: 917489d657451b3efe18181ebcdae646f6b392952f0ee0616b800fa04741b739840d73cd7feb80529df0b077926efadb96f8454602d0d58a2efef1129b4de55f
7
- data.tar.gz: 4f8ed33d63076f70e383069c9b97277d5d2efe315afc92f31c96ff63e209eb3d082d5127409221f810b810312b22a221fdbd803a32500a95f29f764d3b05cd0b
6
+ metadata.gz: 76a7947095f99a17df0607fa139e45949ba4649f6bed2e9e1ee7efe2992863e697c9f0a285060c8003bc8c9282fb21478833859fd583f1fb02a8c54425a0dda4
7
+ data.tar.gz: 6e9d39085c3614245854a19023cdd4cd06d02ac8e20a7eee7f6ac2119b2b4fc78a92cef7f40f9b564b202af3cf4954c9b02f5bb1b68720e4aec0d664e721bf69
@@ -5,10 +5,32 @@ require "rescue_from_duplicate/active_record/version"
5
5
  module RescueFromDuplicate
6
6
  module ActiveRecord
7
7
  end
8
+
9
+ def self.missing_unique_indexes
10
+ ar_subclasses = ObjectSpace.each_object(Class).select{ |klass| klass < ::ActiveRecord::Base }
11
+ klasses = ar_subclasses.select do |klass|
12
+ klass.validators.any? { |v| v.is_a?(::ActiveRecord::Validations::UniquenessValidator) || klass._rescue_from_duplicates.any? }
13
+ end
14
+
15
+ missing_unique_indexes = []
16
+
17
+ klasses.each do |klass|
18
+ klass._rescue_from_duplicate_handlers.each do |handler|
19
+ unique_indexes = klass.connection.indexes(klass.table_name).select(&:unique)
20
+
21
+ unless unique_indexes.any? { |index| index.columns.map(&:to_s).sort == handler.columns }
22
+ missing_unique_indexes << MissingUniqueIndex.new(klass, handler.attributes, handler.columns)
23
+ end
24
+ end
25
+ end
26
+ missing_unique_indexes
27
+ end
8
28
  end
9
29
 
10
30
  require 'rescue_from_duplicate/active_record/extension'
31
+ require 'rescue_from_duplicate/uniqueness_rescuer'
11
32
  require 'rescue_from_duplicate/rescuer'
33
+ require 'rescue_from_duplicate/missing_unique_index'
12
34
 
13
35
  ActiveSupport.on_load(:active_record) do
14
36
  ::ActiveRecord::Base.send :include, RescueFromDuplicate::ActiveRecord::Extension
@@ -8,6 +8,13 @@ module RescueFromDuplicate::ActiveRecord
8
8
  def rescue_from_duplicate(attribute, options = {})
9
9
  self._rescue_from_duplicates += [RescueFromDuplicate::Rescuer.new(attribute, options)]
10
10
  end
11
+
12
+ def _rescue_from_duplicate_handlers
13
+ validator_handlers = self.validators.select { |v| v.is_a?(ActiveRecord::Validations::UniquenessValidator) }.map do |v|
14
+ RescueFromDuplicate::UniquenessRescuer.new(v)
15
+ end
16
+ self._rescue_from_duplicates + validator_handlers
17
+ end
11
18
  end
12
19
 
13
20
  included do
@@ -18,7 +25,7 @@ module RescueFromDuplicate::ActiveRecord
18
25
  def create_or_update(*params, &block)
19
26
  super
20
27
  rescue ActiveRecord::RecordNotUnique => exception
21
- handler = exception_validator(exception) || exception_rescuer(exception)
28
+ handler = exception_handler(exception)
22
29
 
23
30
  raise exception unless handler
24
31
 
@@ -29,36 +36,25 @@ module RescueFromDuplicate::ActiveRecord
29
36
  false
30
37
  end
31
38
 
32
- def exception_validator(exception)
39
+ private
40
+
41
+ def exception_handler(exception)
33
42
  columns = exception_columns(exception)
34
43
 
35
- self._validators.each do |attribute, validators|
36
- validators.each do |validator|
37
- next unless validator.is_a?(ActiveRecord::Validations::UniquenessValidator)
38
- return validator if rescue_with_validator?(columns, validator)
39
- end
44
+ self.class._rescue_from_duplicate_handlers.detect do |handler|
45
+ handler.rescue? && columns == handler.columns
40
46
  end
41
-
42
- nil
43
47
  end
44
48
 
45
- protected
46
-
47
49
  def exception_columns(exception)
48
- columns = case
50
+ case
49
51
  when exception.message =~ /SQLite3::ConstraintException/
50
52
  sqlite3_exception_columns(exception)
51
53
  when exception.message =~ /PG::UniqueViolation/
52
54
  postgresql_exception_columns(exception)
53
55
  else
54
56
  other_exception_columns(exception)
55
- end
56
- end
57
-
58
- def exception_rescuer(exception)
59
- columns = exception_columns(exception)
60
-
61
- _rescue_from_duplicates.detect { |rescuer| rescuer.matches?(columns) }
57
+ end.sort
62
58
  end
63
59
 
64
60
  def postgresql_exception_columns(exception)
@@ -71,19 +67,12 @@ module RescueFromDuplicate::ActiveRecord
71
67
 
72
68
  def extract_columns(columns_string)
73
69
  return unless columns_string
74
- columns_string.split(",").map(&:strip).sort
70
+ columns_string.split(",").map(&:strip)
75
71
  end
76
72
 
77
73
  def other_exception_columns(exception)
78
74
  indexes = self.class.connection.indexes(self.class.table_name)
79
- columns = indexes.detect{ |i| exception.message.include?(i.name) }.try(:columns) || []
80
- columns.sort
81
- end
82
-
83
- def rescue_with_validator?(columns, validator)
84
- validator_columns = (Array(validator.options[:scope]) + validator.attributes).map(&:to_s).sort
85
- return false unless columns == validator_columns
86
- validator.options.fetch(:rescue_from_duplicate) { false }
75
+ indexes.detect{ |i| exception.message.include?(i.name) }.try(:columns) || []
87
76
  end
88
77
  end
89
78
  end
@@ -1,5 +1,5 @@
1
1
  module Activerecord
2
2
  module RescueFromDuplicate
3
- VERSION = "0.0.6"
3
+ VERSION = "0.0.7"
4
4
  end
5
5
  end
@@ -0,0 +1,11 @@
1
+ module RescueFromDuplicate
2
+ class MissingUniqueIndex
3
+ attr_reader :model, :attributes, :columns
4
+
5
+ def initialize(model, attributes, columns)
6
+ @model = model
7
+ @attributes = attributes
8
+ @columns = columns
9
+ end
10
+ end
11
+ end
@@ -1,6 +1,6 @@
1
1
  module RescueFromDuplicate
2
2
  class Rescuer
3
- attr_reader :attributes, :options
3
+ attr_reader :attributes, :options, :columns
4
4
 
5
5
  def initialize(attribute, options)
6
6
  @attributes = [attribute]
@@ -8,8 +8,8 @@ module RescueFromDuplicate
8
8
  @options = options
9
9
  end
10
10
 
11
- def matches?(columns)
12
- @columns == columns.map(&:to_s).sort
11
+ def rescue?
12
+ true
13
13
  end
14
14
  end
15
15
  end
@@ -0,0 +1,23 @@
1
+ module RescueFromDuplicate
2
+ class UniquenessRescuer
3
+ def initialize(validator)
4
+ @validator = validator
5
+ end
6
+
7
+ def rescue?
8
+ @validator.options.fetch(:rescue_from_duplicate, false)
9
+ end
10
+
11
+ def options
12
+ @validator.options
13
+ end
14
+
15
+ def attributes
16
+ @validator.attributes
17
+ end
18
+
19
+ def columns
20
+ (Array(options[:scope]) + attributes).map(&:to_s).sort
21
+ end
22
+ end
23
+ end
@@ -10,67 +10,6 @@ shared_examples 'database error rescuing' do
10
10
  allow(Rescuable).to receive(:connection).and_return(double(indexes: [Rescuable.index]))
11
11
  end
12
12
 
13
- describe "#exception_validator" do
14
- context "validator can be found" do
15
- it "returns the validator" do
16
- expect(subject.exception_validator(uniqueness_exception)).to eq Rescuable.uniqueness_validator
17
- end
18
- end
19
-
20
- context "validator cannot be found" do
21
- before {
22
- Rescuable.stub(_validators: {name: [Rescuable.presence_validator]})
23
- }
24
-
25
- it "returns nil" do
26
- expect(subject.exception_validator(uniqueness_exception)).to be_nil
27
- end
28
- end
29
-
30
- context "validator doesn't specify :rescue_from_duplicate" do
31
- before {
32
- Rescuable.stub(_validators: {name: [Rescuable.uniqueness_validator_without_rescue]})
33
- }
34
-
35
- it "returns nil" do
36
- expect(subject.exception_validator(uniqueness_exception)).to be_nil
37
- end
38
- end
39
-
40
- context "no validator" do
41
- before {
42
- Rescuable.stub(_validators: {})
43
- }
44
-
45
- it "returns nil" do
46
- expect(subject.exception_validator(uniqueness_exception)).to be_nil
47
- end
48
- end
49
-
50
- context "no index on the table" do
51
- before {
52
- Rescuable.stub(index: nil)
53
- Rescuable.stub(connection: double(indexes: []))
54
- }
55
-
56
- let(:message) { super().gsub(/column (.*?) is/, 'column toto is').gsub(/Key \((.*?)\)=/, 'Key (toto)=') }
57
-
58
- it "returns nil" do
59
- expect(subject.exception_validator(uniqueness_exception)).to be_nil
60
- end
61
- end
62
-
63
- context "columns part of the index of another table" do
64
- before {
65
- subject.stub(exception_columns: ['foo', 'baz'])
66
- }
67
-
68
- it "returns nil" do
69
- expect(subject.exception_validator(uniqueness_exception)).to be_nil
70
- end
71
- end
72
- end
73
-
74
13
  describe "#create_or_update when the validation fails" do
75
14
  before { Base.stub(exception: uniqueness_exception) }
76
15
 
@@ -82,7 +21,7 @@ shared_examples 'database error rescuing' do
82
21
  end
83
22
 
84
23
  context "when the validator is not present" do
85
- before { Rescuable.stub(_validators: {name: [Rescuable.presence_validator]}) }
24
+ before { Rescuable.stub(validators: [Rescuable.presence_validator]) }
86
25
 
87
26
  it "raises an exception" do
88
27
  expect{ subject.create_or_update }.to raise_error(ActiveRecord::RecordNotUnique)
@@ -1,17 +1,18 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe RescueFromDuplicate::Rescuer do
4
- subject { RescueFromDuplicate::Rescuer.new(:shop_id, scope: :name) }
4
+ subject { RescueFromDuplicate::Rescuer.new(:name, scope: [:type, :shop_id], message: "Derp!") }
5
5
 
6
- context "#matches?" do
7
- it 'is true when the columns are the same' do
8
- expect(subject.matches?(["shop_id", "name"])).to be true
9
- expect(subject.matches?(["name", "shop_id"])).to be true
10
- end
6
+ it "always rescues" do
7
+ expect(subject.rescue?).to eq true
8
+ end
11
9
 
12
- it 'is false when the columns are not the same' do
13
- expect(subject.matches?(["shop_id", "toto"])).to be false
14
- end
10
+ it "sorts the columns" do
11
+ expect(subject.columns).to eq ['name', 'shop_id', 'type']
12
+ end
13
+
14
+ it "returns the options" do
15
+ expect(subject.options).to eq scope: [:type, :shop_id], message: "Derp!"
15
16
  end
16
17
  end
17
18
 
@@ -43,28 +43,21 @@ module RescueFromDuplicate
43
43
  send(attribute)
44
44
  end
45
45
 
46
- def _validators
47
- self.class._validators
48
- end
49
-
50
46
  def errors
51
47
  @errors ||= ActiveModel::Errors.new(self)
52
48
  end
53
49
 
54
- def self._validators
55
- @validators ||= {
56
- name:
57
- [
58
- uniqueness_validator,
59
- presence_validator
60
- ]
61
- }
50
+ def self.validators
51
+ @validators ||= [
52
+ uniqueness_validator,
53
+ presence_validator
54
+ ]
62
55
  end
63
56
 
64
57
  def self.uniqueness_validator
65
58
  @uniqueness_validator ||= ::ActiveRecord::Validations::UniquenessValidator.new(
66
59
  attributes: [:name],
67
- case_sensitive: true, scope: [:shop_id, :type],
60
+ case_sensitive: true, scope: [:type, :shop_id],
68
61
  rescue_from_duplicate: true
69
62
  ).tap { |o| o.setup(self) if o.respond_to?(:setup) }
70
63
  end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ describe RescueFromDuplicate::UniquenessRescuer do
4
+ context "common" do
5
+ subject { RescueFromDuplicate::UniquenessRescuer.new(Rescuable.uniqueness_validator) }
6
+
7
+ it "returns the options" do
8
+ options = { case_sensitive: true, scope: [:type, :shop_id], rescue_from_duplicate: true }
9
+ expect(subject.options).to eq options
10
+ end
11
+
12
+ it "sorts the columns" do
13
+ expect(subject.columns).to eq ['name', 'shop_id', 'type']
14
+ end
15
+
16
+ it "returns the attributes" do
17
+ expect(subject.attributes).to eq [:name]
18
+ end
19
+ end
20
+
21
+ context "validator with rescue" do
22
+ subject { RescueFromDuplicate::UniquenessRescuer.new(Rescuable.uniqueness_validator) }
23
+ it "rescues" do
24
+ expect(subject.rescue?).to eq true
25
+ end
26
+ end
27
+
28
+ context "validator without rescue" do
29
+ subject { RescueFromDuplicate::UniquenessRescuer.new(Rescuable.uniqueness_validator_without_rescue) }
30
+ it "doesn't rescues" do
31
+ expect(subject.rescue?).to eq false
32
+ end
33
+ end
34
+ end
@@ -45,18 +45,59 @@ shared_examples 'a model with rescued uniqueness validator' do
45
45
  end
46
46
  end
47
47
 
48
+ shared_examples 'missing index finding' do
49
+ describe do
50
+ context 'all indexes are satisfied' do
51
+ it 'returns an empty array' do
52
+ expect(RescueFromDuplicate.missing_unique_indexes).to be_empty
53
+ end
54
+ end
55
+
56
+ context 'indexes are missing' do
57
+ before {
58
+ described_class.stub(_rescue_from_duplicate_handlers: [
59
+ RescueFromDuplicate::UniquenessRescuer.new(
60
+ ::ActiveRecord::Validations::UniquenessValidator.new(
61
+ attributes: [:name],
62
+ case_sensitive: true, scope: [:titi, :toto],
63
+ )
64
+ ),
65
+ RescueFromDuplicate::Rescuer.new(:name, scope: [:hello])
66
+ ])
67
+ }
68
+
69
+ it 'returns the missing indexes' do
70
+ missing_unique_indexes = RescueFromDuplicate.missing_unique_indexes.select { |mi| mi.model == described_class }
71
+ expect(missing_unique_indexes).not_to be_empty
72
+
73
+ expect(missing_unique_indexes.first.model).to eq described_class
74
+ expect(missing_unique_indexes.last.model).to eq described_class
75
+
76
+ expect(missing_unique_indexes.first.attributes).to eq [:name]
77
+ expect(missing_unique_indexes.last.attributes).to eq [:name]
78
+
79
+ expect(missing_unique_indexes.first.columns).to eq ["name", "titi", "toto"]
80
+ expect(missing_unique_indexes.last.columns).to eq ["hello", "name"]
81
+ end
82
+ end
83
+ end
84
+ end
85
+
48
86
  describe Sqlite3Model do
49
87
  it_behaves_like 'a model with rescued uniqueness validator'
88
+ it_behaves_like 'missing index finding'
50
89
  end
51
90
 
52
91
  if defined?(MysqlModel)
53
92
  describe MysqlModel do
54
93
  it_behaves_like 'a model with rescued uniqueness validator'
94
+ it_behaves_like 'missing index finding'
55
95
  end
56
96
  end
57
97
 
58
98
  if defined?(PostgresqlModel)
59
99
  describe PostgresqlModel do
60
100
  it_behaves_like 'a model with rescued uniqueness validator'
101
+ it_behaves_like 'missing index finding'
61
102
  end
62
103
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-rescue_from_duplicate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Guillaume Malette
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-08 00:00:00.000000000 Z
11
+ date: 2014-10-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -155,7 +155,9 @@ files:
155
155
  - lib/rescue_from_duplicate/active_record.rb
156
156
  - lib/rescue_from_duplicate/active_record/extension.rb
157
157
  - lib/rescue_from_duplicate/active_record/version.rb
158
+ - lib/rescue_from_duplicate/missing_unique_index.rb
158
159
  - lib/rescue_from_duplicate/rescuer.rb
160
+ - lib/rescue_from_duplicate/uniqueness_rescuer.rb
159
161
  - shipit.rubygems.yml
160
162
  - spec/gemfiles/Gemfile.ar-3.2
161
163
  - spec/gemfiles/Gemfile.ar-4.0
@@ -165,6 +167,7 @@ files:
165
167
  - spec/rescuer_spec.rb
166
168
  - spec/spec_helper.rb
167
169
  - spec/support/model.rb
170
+ - spec/uniqueness_rescuer_spec.rb
168
171
  - spec/validator_spec.rb
169
172
  homepage: https://github.com/Shopify/activerecord-rescue_from_duplicate
170
173
  licenses:
@@ -200,4 +203,5 @@ test_files:
200
203
  - spec/rescuer_spec.rb
201
204
  - spec/spec_helper.rb
202
205
  - spec/support/model.rb
206
+ - spec/uniqueness_rescuer_spec.rb
203
207
  - spec/validator_spec.rb