activerecord-rescue_from_duplicate 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
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