activerecord-rescue_from_duplicate 0.0.2 → 0.0.4

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: 4d59c5d63f6929f47bcf86422995f8cd95893edf
4
- data.tar.gz: 547f6c3633d0df084d64cc85c5afbd50a45db1a1
3
+ metadata.gz: e499e51bfc3ec5a7d05bd7835a0cbb3916607905
4
+ data.tar.gz: d6c76963003fc3bac1b594f490680ad622251b3c
5
5
  SHA512:
6
- metadata.gz: 8195739d842dc67951c013505698d11ac0fb591dd70fa1723eb17cc153c1b68d92b9dbf8ddd750de1043e69e64b4ae6d8b792c4d57e2a4371c62bff52d0012d6
7
- data.tar.gz: 77fe96c1d679bb986c413713c645575e59539335d1a8d1b5acfc584d35c2762c144a386c8fa9fd7d2c5befa8a32822a3b181e5c0488920bfa01c94005465f1f7
6
+ metadata.gz: 9dbc186269181a05853c12fed297986f0529dfb26bc3297a1758fc304da0c5966a634ed84044ccd0c67c08f4456e0f32d76938473845a45fe123982d3d54511c
7
+ data.tar.gz: 224278194bf55ade9ba082e1841513faa2b29ecbef1918887476dcb9bd126636e5d0c0e24a0ab9ec6b172ebc62d8f81b4040a0bc501fc96e85f29ba1d84d0fe3
data/.travis.yml CHANGED
@@ -1,9 +1,11 @@
1
1
  rvm:
2
2
  - 1.9.3
3
3
  - 2.0.0
4
+ - 2.1.2
4
5
  gemfile:
5
6
  - spec/gemfiles/Gemfile.ar-3.2
6
7
  - spec/gemfiles/Gemfile.ar-4.0
8
+ - spec/gemfiles/Gemfile.ar-4.1
7
9
  - spec/gemfiles/Gemfile.ar-edge
8
10
 
9
11
  before_script:
@@ -11,16 +13,8 @@ before_script:
11
13
  - psql -c 'create database rescue_from_duplicate;' -U postgres
12
14
 
13
15
  env:
14
- - POSTGRES=1
15
- - MYSQL=1
16
+ - POSTGRES=1 MYSQL=1
16
17
 
17
18
  matrix:
18
19
  allow_failures:
19
20
  - gemfile: gemfiles/Gemfile.ar-edge
20
- - env: POSTGRES=1
21
- - gemfile: gemfiles/Gemfile.ar-edge
22
- env: MYSQL=1
23
- rvm: 1.9.3
24
- - gemfile: gemfiles/Gemfile.ar-edge
25
- env: MYSQL=1
26
- rvm: 2.0.0
data/README.md CHANGED
@@ -2,53 +2,74 @@
2
2
 
3
3
  [![Build Status](https://travis-ci.org/Shopify/activerecord-rescue_from_duplicate.png?branch=master)](https://travis-ci.org/Shopify/activerecord-rescue_from_duplicate)
4
4
 
5
- This gem will rescue from MySQL and Sqlite errors when trying to insert records that fail uniqueness validation.
6
- PostgreSQL is not supported at the moment because of the errors raised when using prepared statements.
5
+ This gem will rescue SQL errors when trying to insert records that fail uniqueness validation.
7
6
 
8
7
  It complements `:validates_uniqueness_of` and will add appropriate errors.
9
8
 
10
- **Note:**
9
+ Additionally, a macro allows you to assume that the record will be unique and rescue gracefully otherwise.
11
10
 
12
- * All `before_*` filters will have been run.
13
- * Unlike failed validation, `ActiveRecord::RecordNotSaved` will be raised when using `create!`, `save!` or other `!` methods.
11
+ Tested with:
14
12
 
15
- ## Installation
16
-
17
- Add this line to your application's Gemfile:
13
+ - ActiveRecord: 3.2, 4.0, 4.1, edge
14
+ - Ruby 1.9.3, 2.0.0, 2.1.2
15
+ - MySQL, PostgreSQL, Sqlite3
18
16
 
19
- gem 'activerecord-rescue_from_duplicate'
20
-
21
- And then execute:
17
+ **Note:**
22
18
 
23
- $ bundle
19
+ * `after_validation`, `before_save`, `before_create` will have been run. Make sure they don't have undesired side-effects.
20
+ * Unlike failed validation, `ActiveRecord::RecordNotSaved` will be raised when using `create!`, `save!` or other `!` methods.
24
21
 
25
- Or install it yourself as:
22
+ ## Usage
26
23
 
27
- $ gem install activerecord-rescue_from_duplicate
24
+ ### With validation
28
25
 
29
- ## Usage
26
+ Add the `rescue_from_duplicate: true` to any regular uniqueness validation.
30
27
 
31
- Add the `:rescue_from_duplicate => true` to any regular uniqueness validation.
28
+ This will use the Rails standard validation, performing a `SELECT` to ensure the record is valid. In the case of a race condition, an error will be added to the model, and no exception will be thrown.
32
29
 
33
30
  ```ruby
34
31
  class ModelWithUniquenessValidator < ActiveRecord::Base
35
- validates_uniqueness_of :name, :scope => :shop_id, :rescue_from_duplicate => true
32
+ validates_uniqueness_of :name, scope: :shop_id, rescue_from_duplicate: true
36
33
  end
37
34
  ```
38
35
 
39
36
  If two of this statement go in at the same time, and the original validation on uniqueness of name passes, the DBMS will raise an duplicate record error.
40
37
 
41
38
  ```ruby
42
- a = ModelWithUniquenessValidator.create(:name => "name")
39
+ a = ModelWithUniquenessValidator.create(name: "name")
43
40
 
44
41
  # in a different thread, causing race condition
45
- b = ModelWithUniquenessValidator.create(:name => "name")
42
+ b = ModelWithUniquenessValidator.create(name: "name")
46
43
 
47
44
  a.persisted? #=> true
48
45
  b.persisted? #=> false
49
46
  b.errors[:name] #=> ["has already been taken"]
50
47
  ```
51
48
 
49
+ ### Without validation
50
+
51
+ You can use this if you don't need to check that the record is unique before attempting to insert it. It will not add any validation to the model, but it will add an error if the persistance fails.
52
+
53
+ ```ruby
54
+ class ModelWithUniqueToken < ActiveRecord::Base
55
+ rescue_from_duplicate :token, scope: :shop_id
56
+ end
57
+ ```
58
+
59
+ ## Installation
60
+
61
+ Add this line to your application's Gemfile:
62
+
63
+ gem 'activerecord-rescue_from_duplicate'
64
+
65
+ And then execute:
66
+
67
+ $ bundle
68
+
69
+ Or install it yourself as:
70
+
71
+ $ gem install activerecord-rescue_from_duplicate
72
+
52
73
  ## Contributing
53
74
 
54
75
  1. Fork it
data/Rakefile CHANGED
@@ -7,7 +7,7 @@ task :default => :spec
7
7
 
8
8
  namespace :spec do
9
9
  task :all do
10
- %w(3.2 4.0 edge).each do |ar_version|
10
+ %w(3.2 4.0 4.1 edge).each do |ar_version|
11
11
  system(
12
12
  {
13
13
  "BUNDLE_GEMFILE" => "spec/gemfiles/Gemfile.ar-#{ar_version}",
@@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
7
7
  spec.name = "activerecord-rescue_from_duplicate"
8
8
  spec.version = Activerecord::RescueFromDuplicate::VERSION
9
9
  spec.authors = ["Guillaume Malette"]
10
- spec.email = ["guillaume@jadedpixel.com"]
10
+ spec.email = ["guillaume@shopify.com"]
11
11
  spec.description = %q{Rescue from MySQL and Sqlite duplicate errors}
12
12
  spec.summary = %q{Rescue from MySQL and Sqlite duplicate errors when trying to insert records that fail uniqueness validation}
13
13
  spec.homepage = "https://github.com/Shopify/activerecord-rescue_from_duplicate"
@@ -22,11 +22,10 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.add_development_dependency "bundler", "~> 1.3"
24
24
  spec.add_development_dependency "rake"
25
- spec.add_development_dependency 'coveralls'
25
+ spec.add_development_dependency 'simplecov'
26
26
  spec.add_development_dependency 'sqlite3'
27
- spec.add_development_dependency 'pg'
28
27
  spec.add_development_dependency 'mysql2'
29
28
  spec.add_development_dependency 'rspec'
29
+ spec.add_development_dependency 'pg'
30
30
  spec.add_development_dependency 'pry'
31
- spec.add_development_dependency 'pry-debugger'
32
31
  end
@@ -8,6 +8,7 @@ module RescueFromDuplicate
8
8
  end
9
9
 
10
10
  require 'rescue_from_duplicate/active_record/extension'
11
+ require 'rescue_from_duplicate/rescuer'
11
12
 
12
13
  ActiveSupport.on_load(:active_record) do
13
14
  ::ActiveRecord::Base.send :include, RescueFromDuplicate::ActiveRecord::Extension
@@ -2,51 +2,85 @@ require 'active_support/core_ext/class'
2
2
 
3
3
  module RescueFromDuplicate::ActiveRecord
4
4
  module Extension
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def rescue_from_duplicate(attribute, options = {})
9
+ self._rescue_from_duplicates += [RescueFromDuplicate::Rescuer.new(attribute, options)]
10
+ end
11
+ end
12
+
13
+ included do
14
+ class_attribute :_rescue_from_duplicates
15
+ self._rescue_from_duplicates = []
16
+ end
17
+
5
18
  def create_or_update(*params, &block)
6
19
  super
7
20
  rescue ActiveRecord::RecordNotUnique => exception
8
- validator = exception_validator(exception)
21
+ handler = exception_validator(exception) || exception_rescuer(exception)
9
22
 
10
- raise exception unless validator
23
+ raise exception unless handler
11
24
 
12
- attribute = validator.attributes.first
13
- options = validator.options.except(:case_sensitive, :scope).merge(:value => self.send(:read_attribute_for_validation, attribute))
25
+ attribute = handler.attributes.first
26
+ options = handler.options.except(:case_sensitive, :scope).merge(value: self.send(:read_attribute_for_validation, attribute))
14
27
 
15
28
  self.errors.add(attribute, :taken, options)
16
29
  false
17
30
  end
18
31
 
19
32
  def exception_validator(exception)
20
- columns = exception_columns(exception).sort
33
+ columns = exception_columns(exception)
21
34
 
22
35
  self._validators.each do |attribute, validators|
23
36
  validators.each do |validator|
24
37
  next unless validator.is_a?(ActiveRecord::Validations::UniquenessValidator)
25
- return validator if rescue_with_validator(columns, validator)
38
+ return validator if rescue_with_validator?(columns, validator)
26
39
  end
27
40
  end
28
41
 
29
42
  nil
30
43
  end
31
44
 
45
+ protected
46
+
32
47
  def exception_columns(exception)
33
- exception.message =~ /SQLite3::ConstraintException/ ? sqlite3_exception_columns(exception) : other_exception_columns(exception)
48
+ columns = case
49
+ when exception.message =~ /SQLite3::ConstraintException/
50
+ sqlite3_exception_columns(exception)
51
+ when exception.message =~ /PG::UniqueViolation/
52
+ postgresql_exception_columns(exception)
53
+ else
54
+ other_exception_columns(exception)
55
+ end
34
56
  end
35
57
 
36
- protected
58
+ def exception_rescuer(exception)
59
+ columns = exception_columns(exception)
60
+
61
+ _rescue_from_duplicates.detect { |rescuer| rescuer.matches?(columns) }
62
+ end
63
+
64
+ def postgresql_exception_columns(exception)
65
+ extract_columns(exception.message[/Key \((.*?)\)=\(.*?\) already exists./, 1])
66
+ end
37
67
 
38
68
  def sqlite3_exception_columns(exception)
39
- columns = exception.message[/column (.*) is not unique/, 1]
40
- return unless columns
41
- columns.split(",").map(&:strip)
69
+ extract_columns(exception.message[/columns? (.*) (?:is|are) not unique/, 1])
70
+ end
71
+
72
+ def extract_columns(columns_string)
73
+ return unless columns_string
74
+ columns_string.split(",").map(&:strip).sort
42
75
  end
43
76
 
44
77
  def other_exception_columns(exception)
45
78
  indexes = self.class.connection.indexes(self.class.table_name)
46
- indexes.detect{ |i| exception.message.include?(i.name) }.try(:columns) || []
79
+ columns = indexes.detect{ |i| exception.message.include?(i.name) }.try(:columns) || []
80
+ columns.sort
47
81
  end
48
82
 
49
- def rescue_with_validator(columns, validator)
83
+ def rescue_with_validator?(columns, validator)
50
84
  validator_columns = (Array(validator.options[:scope]) + validator.attributes).map(&:to_s).sort
51
85
  return false unless columns == validator_columns
52
86
  validator.options.fetch(:rescue_from_duplicate) { false }
@@ -1,5 +1,5 @@
1
1
  module Activerecord
2
2
  module RescueFromDuplicate
3
- VERSION = "0.0.2"
3
+ VERSION = "0.0.4"
4
4
  end
5
5
  end
@@ -0,0 +1,15 @@
1
+ module RescueFromDuplicate
2
+ class Rescuer
3
+ attr_reader :attributes, :options
4
+
5
+ def initialize(attribute, options)
6
+ @attributes = [attribute]
7
+ @columns = [attribute, *Array(options[:scope])].map(&:to_s)
8
+ @options = options
9
+ end
10
+
11
+ def matches?(columns)
12
+ @columns == columns.map(&:to_s).sort
13
+ end
14
+ end
15
+ end
@@ -0,0 +1 @@
1
+ # using the default shipit config
@@ -7,4 +7,4 @@ gem 'rspec'
7
7
  gem 'sqlite3'
8
8
  gem 'pg', '~> 0.11'
9
9
  gem 'mysql2'
10
- gem 'coveralls', require: false
10
+ gem 'simplecov', require: false
@@ -7,4 +7,4 @@ gem 'rspec'
7
7
  gem 'sqlite3'
8
8
  gem 'pg', '~> 0.11'
9
9
  gem 'mysql2'
10
- gem 'coveralls', require: false
10
+ gem 'simplecov', require: false
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', '~> 4.0.0'
4
+ gem 'bundler', '~> 1.3'
5
+ gem 'rake'
6
+ gem 'rspec'
7
+ gem 'sqlite3'
8
+ gem 'pg', '~> 0.11'
9
+ gem 'mysql2'
10
+ gem 'simplecov', require: false
@@ -9,4 +9,4 @@ gem 'rspec'
9
9
  gem 'sqlite3'
10
10
  gem 'pg', '~> 0.11'
11
11
  gem 'mysql2'
12
- gem 'coveralls', require: false
12
+ gem 'simplecov', require: false
@@ -7,24 +7,7 @@ shared_examples 'database error rescuing' do
7
7
  subject { Rescuable.new }
8
8
 
9
9
  before do
10
- Rescuable.stub(:connection => double(:indexes => [Rescuable.index]))
11
- end
12
-
13
- describe "#exception_columns" do
14
- context "index cannot be found" do
15
- let(:message) { super().gsub(/index_\w+/, "index_toto").gsub(/column .* is/, 'column toto is') }
16
- let(:exception) { ActiveRecord::RecordNotUnique.new(message, nil) }
17
-
18
- it "returns an array" do
19
- expect(subject.exception_columns(exception)).to be_a Array
20
- end
21
- end
22
-
23
- context "index can be found" do
24
- it "returns the columns" do
25
- expect(subject.exception_columns(uniqueness_exception).sort).to eq ["shop_id", "type", "name"].sort
26
- end
27
- end
10
+ allow(Rescuable).to receive(:connection).and_return(double(indexes: [Rescuable.index]))
28
11
  end
29
12
 
30
13
  describe "#exception_validator" do
@@ -36,7 +19,7 @@ shared_examples 'database error rescuing' do
36
19
 
37
20
  context "validator cannot be found" do
38
21
  before {
39
- Rescuable.stub(:_validators => {:name => [Rescuable.presence_validator]})
22
+ Rescuable.stub(_validators: {name: [Rescuable.presence_validator]})
40
23
  }
41
24
 
42
25
  it "returns nil" do
@@ -46,7 +29,7 @@ shared_examples 'database error rescuing' do
46
29
 
47
30
  context "validator doesn't specify :rescue_from_duplicate" do
48
31
  before {
49
- Rescuable.stub(:_validators => {:name => [Rescuable.uniqueness_validator_without_rescue]})
32
+ Rescuable.stub(_validators: {name: [Rescuable.uniqueness_validator_without_rescue]})
50
33
  }
51
34
 
52
35
  it "returns nil" do
@@ -56,7 +39,7 @@ shared_examples 'database error rescuing' do
56
39
 
57
40
  context "no validator" do
58
41
  before {
59
- Rescuable.stub(:_validators => {})
42
+ Rescuable.stub(_validators: {})
60
43
  }
61
44
 
62
45
  it "returns nil" do
@@ -66,11 +49,11 @@ shared_examples 'database error rescuing' do
66
49
 
67
50
  context "no index on the table" do
68
51
  before {
69
- Rescuable.stub(:index => nil)
70
- Rescuable.stub(:connection => double(:indexes => []))
52
+ Rescuable.stub(index: nil)
53
+ Rescuable.stub(connection: double(indexes: []))
71
54
  }
72
55
 
73
- let(:message) { super().gsub(/column (.*) is/, 'column toto is') }
56
+ let(:message) { super().gsub(/column (.*?) is/, 'column toto is').gsub(/Key \((.*?)\)=/, 'Key (toto)=') }
74
57
 
75
58
  it "returns nil" do
76
59
  expect(subject.exception_validator(uniqueness_exception)).to be_nil
@@ -79,7 +62,7 @@ shared_examples 'database error rescuing' do
79
62
 
80
63
  context "columns part of the index of another table" do
81
64
  before {
82
- subject.stub(:exception_columns => ['foo', 'baz'])
65
+ subject.stub(exception_columns: ['foo', 'baz'])
83
66
  }
84
67
 
85
68
  it "returns nil" do
@@ -89,7 +72,7 @@ shared_examples 'database error rescuing' do
89
72
  end
90
73
 
91
74
  describe "#create_or_update when the validation fails" do
92
- before { Base.stub(:exception => uniqueness_exception) }
75
+ before { Base.stub(exception: uniqueness_exception) }
93
76
 
94
77
  context "when the validator is present" do
95
78
  it "adds an error to the model" do
@@ -99,13 +82,26 @@ shared_examples 'database error rescuing' do
99
82
  end
100
83
 
101
84
  context "when the validator is not present" do
102
- before { Rescuable.stub(:_validators => {:name => [Rescuable.presence_validator]}) }
85
+ before { Rescuable.stub(_validators: {name: [Rescuable.presence_validator]}) }
103
86
 
104
87
  it "raises an exception" do
105
88
  expect{ subject.create_or_update }.to raise_error(ActiveRecord::RecordNotUnique)
106
89
  end
107
90
  end
108
91
  end
92
+
93
+ describe "#create_or_update when using rescuer without validation" do
94
+ before {
95
+ Rescuable.stub(_validators: {})
96
+ Rescuable.stub(_rescue_from_duplicates: [Rescuable.uniqueness_rescuer])
97
+ Base.stub(exception: uniqueness_exception)
98
+ }
99
+
100
+ it "adds an error to the model" do
101
+ subject.create_or_update
102
+ expect(subject.errors[:name]).to eq ["is not unique by type and shop id"]
103
+ end
104
+ end
109
105
  end
110
106
 
111
107
  describe RescueFromDuplicate::ActiveRecord do
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe RescueFromDuplicate::Rescuer do
4
+ subject { RescueFromDuplicate::Rescuer.new(:name, scope: :shop_id) }
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
+ end
10
+
11
+ it 'is false when the columns are not the same' do
12
+ expect(subject.matches?(["shop_id", "toto"])).to be false
13
+ end
14
+ end
15
+ end
16
+
17
+ shared_examples 'a model with rescued unique error without validator' do
18
+ describe 'create!' do
19
+ context 'when catching a race condition' do
20
+ before {
21
+ described_class.create!(relation_id: 1, handle: 'toto')
22
+ }
23
+
24
+ it 'adds an error on the model' do
25
+ model = described_class.create(relation_id: 1, handle: 'toto')
26
+ expect(model.errors[:handle]).to eq(["handle must be unique for this relation"])
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ describe Sqlite3Model do
33
+ it_behaves_like 'a model with rescued unique error without validator'
34
+ end
35
+
36
+ if defined?(MysqlModel)
37
+ describe MysqlModel do
38
+ it_behaves_like 'a model with rescued unique error without validator'
39
+ end
40
+ end
41
+
42
+ if defined?(PostgresqlModel)
43
+ describe PostgresqlModel do
44
+ it_behaves_like 'a model with rescued unique error without validator'
45
+ end
46
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,23 +1,20 @@
1
+ require 'simplecov'
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ SimpleCov.start do
7
+ add_filter '/spec/'
8
+ end
9
+
1
10
  require 'active_record'
2
11
  require 'activerecord-rescue_from_duplicate'
3
12
 
4
13
  begin
5
14
  require 'pry'
6
- require 'pry-debugger'
7
15
  rescue LoadError
8
16
  end
9
17
 
10
- require 'simplecov'
11
- require 'coveralls'
12
-
13
- # lib = File.expand_path('../lib', __FILE__)
14
- # $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
15
-
16
- # SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
17
- # SimpleCov::Formatter::HTMLFormatter,
18
- # Coveralls::SimpleCov::Formatter
19
- # ]
20
- # SimpleCov.start
21
18
 
22
19
 
23
20
  module RescueFromDuplicate
@@ -56,7 +53,7 @@ module RescueFromDuplicate
56
53
 
57
54
  def self._validators
58
55
  @validators ||= {
59
- :name =>
56
+ name:
60
57
  [
61
58
  uniqueness_validator,
62
59
  presence_validator
@@ -66,21 +63,27 @@ module RescueFromDuplicate
66
63
 
67
64
  def self.uniqueness_validator
68
65
  @uniqueness_validator ||= ::ActiveRecord::Validations::UniquenessValidator.new(
69
- :attributes => [:name],
70
- :case_sensitive => true, :scope => [:shop_id, :type],
71
- :rescue_from_duplicate => true
66
+ attributes: [:name],
67
+ case_sensitive: true, scope: [:shop_id, :type],
68
+ rescue_from_duplicate: true
72
69
  ).tap { |o| o.setup(self) if o.respond_to?(:setup) }
73
70
  end
74
71
 
75
72
  def self.uniqueness_validator_without_rescue
76
73
  @uniqueness_validator_without_rescue ||= ::ActiveRecord::Validations::UniquenessValidator.new(
77
- :attributes => [:name],
78
- :case_sensitive => true, :scope => [:shop_id, :type]
74
+ attributes: [:name],
75
+ case_sensitive: true, scope: [:shop_id, :type]
79
76
  ).tap { |o| o.setup(self) if o.respond_to?(:setup) }
80
77
  end
81
78
 
79
+ def self.uniqueness_rescuer
80
+ @uniqueness_rescuer ||= RescueFromDuplicate::Rescuer.new(
81
+ :name, scope: [:shop_id, :type], message: "is not unique by type and shop id"
82
+ )
83
+ end
84
+
82
85
  def self.presence_validator
83
- @presence_validator ||= ActiveModel::Validations::PresenceValidator.new(:attributes => [:name])
86
+ @presence_validator ||= ActiveModel::Validations::PresenceValidator.new(attributes: [:name])
84
87
  end
85
88
 
86
89
  def self.index
@@ -107,7 +110,6 @@ Dir[File.expand_path(File.join(File.dirname(__FILE__), 'support', '**', '*.rb'))
107
110
  #
108
111
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
109
112
  RSpec.configure do |config|
110
- config.treat_symbols_as_metadata_keys_with_true_values = true
111
113
  config.run_all_when_everything_filtered = true
112
114
  config.filter_run :focus
113
115
 
@@ -17,12 +17,16 @@ class CreateAllTables < ActiveRecord::Migration
17
17
  execute "drop table if exists #{name}"
18
18
 
19
19
  create_table(name, *args) do |t|
20
+ t.integer :relation_id
21
+ t.string :handle
22
+
20
23
  t.string :name
21
24
  t.integer :size
22
25
  end
23
26
 
24
- add_index name, :name, :unique => true
25
- add_index name, :size, :unique => true
27
+ add_index name, [:relation_id, :handle], unique: true
28
+ add_index name, :name, unique: true
29
+ add_index name, :size, unique: true
26
30
  end
27
31
 
28
32
  def self.up
@@ -49,8 +53,10 @@ module TestModel
49
53
  extend ActiveSupport::Concern
50
54
 
51
55
  included do
52
- validates_uniqueness_of :name, :rescue_from_duplicate => true
53
- validates_uniqueness_of :size
56
+ rescue_from_duplicate :handle, scope: :relation_id, message: "handle must be unique for this relation"
57
+
58
+ validates_uniqueness_of :name, rescue_from_duplicate: true, allow_nil: true
59
+ validates_uniqueness_of :size, allow_nil: true
54
60
  end
55
61
  end
56
62
 
@@ -1,22 +1,22 @@
1
1
  require 'spec_helper'
2
2
 
3
- shared_examples 'a model with rescued unique' do
3
+ shared_examples 'a model with rescued uniqueness validator' do
4
4
  describe 'create!' do
5
5
  context 'when catching a race condition' do
6
6
 
7
7
  before(:each) {
8
- ActiveRecord::Validations::UniquenessValidator.any_instance.stub(:validate_each => nil)
9
- described_class.create!(:name => 'toto', :size => 5)
8
+ ActiveRecord::Validations::UniquenessValidator.any_instance.stub(validate_each: nil)
9
+ described_class.create!(name: 'toto', size: 5)
10
10
  }
11
11
 
12
12
  it 'raises an ActiveRecord::RecordNotSaved error' do
13
- expect{ described_class.create!(:name => 'toto') }.to raise_error(ActiveRecord::RecordNotSaved)
13
+ expect{ described_class.create!(name: 'toto') }.to raise_error(ActiveRecord::RecordNotSaved)
14
14
  end
15
15
 
16
16
  it "doesn't save the record" do
17
17
  expect{
18
18
  begin
19
- described_class.create!(:name => 'toto')
19
+ described_class.create!(name: 'toto')
20
20
  rescue ActiveRecord::RecordNotSaved
21
21
  # NOOP
22
22
  end
@@ -27,8 +27,8 @@ shared_examples 'a model with rescued unique' do
27
27
  expect {
28
28
  begin
29
29
  described_class.transaction do
30
- described_class.create(:name => 'not toto', :size => 55)
31
- described_class.create!(:name => 'toto')
30
+ described_class.create(name: 'not toto', size: 55)
31
+ described_class.create!(name: 'toto')
32
32
  end
33
33
  rescue ActiveRecord::RecordNotSaved
34
34
  # NOOP
@@ -39,24 +39,24 @@ shared_examples 'a model with rescued unique' do
39
39
 
40
40
  context "with no race condition" do
41
41
  it 'saves the model' do
42
- expect{ described_class.create!(:name => 'toto') }.to change(described_class, :count).by(1)
42
+ expect{ described_class.create!(name: 'toto') }.to change(described_class, :count).by(1)
43
43
  end
44
44
  end
45
45
  end
46
46
  end
47
47
 
48
48
  describe Sqlite3Model do
49
- it_behaves_like 'a model with rescued unique'
49
+ it_behaves_like 'a model with rescued uniqueness validator'
50
50
  end
51
51
 
52
52
  if defined?(MysqlModel)
53
53
  describe MysqlModel do
54
- it_behaves_like 'a model with rescued unique'
54
+ it_behaves_like 'a model with rescued uniqueness validator'
55
55
  end
56
56
  end
57
57
 
58
58
  if defined?(PostgresqlModel)
59
59
  describe PostgresqlModel do
60
- it_behaves_like 'a model with rescued unique'
60
+ it_behaves_like 'a model with rescued uniqueness validator'
61
61
  end
62
62
  end
metadata CHANGED
@@ -1,165 +1,151 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-rescue_from_duplicate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Guillaume Malette
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-12-17 00:00:00.000000000 Z
11
+ date: 2014-09-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '>='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '3.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - '>='
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '3.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ~>
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
33
  version: '1.3'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ~>
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.3'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - '>='
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - '>='
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: coveralls
56
+ name: simplecov
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - '>='
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - '>='
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: sqlite3
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - '>='
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
75
  version: '0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - '>='
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: pg
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - '>='
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - '>='
80
+ - - ">="
95
81
  - !ruby/object:Gem::Version
96
82
  version: '0'
97
83
  - !ruby/object:Gem::Dependency
98
84
  name: mysql2
99
85
  requirement: !ruby/object:Gem::Requirement
100
86
  requirements:
101
- - - '>='
87
+ - - ">="
102
88
  - !ruby/object:Gem::Version
103
89
  version: '0'
104
90
  type: :development
105
91
  prerelease: false
106
92
  version_requirements: !ruby/object:Gem::Requirement
107
93
  requirements:
108
- - - '>='
94
+ - - ">="
109
95
  - !ruby/object:Gem::Version
110
96
  version: '0'
111
97
  - !ruby/object:Gem::Dependency
112
98
  name: rspec
113
99
  requirement: !ruby/object:Gem::Requirement
114
100
  requirements:
115
- - - '>='
101
+ - - ">="
116
102
  - !ruby/object:Gem::Version
117
103
  version: '0'
118
104
  type: :development
119
105
  prerelease: false
120
106
  version_requirements: !ruby/object:Gem::Requirement
121
107
  requirements:
122
- - - '>='
108
+ - - ">="
123
109
  - !ruby/object:Gem::Version
124
110
  version: '0'
125
111
  - !ruby/object:Gem::Dependency
126
- name: pry
112
+ name: pg
127
113
  requirement: !ruby/object:Gem::Requirement
128
114
  requirements:
129
- - - '>='
115
+ - - ">="
130
116
  - !ruby/object:Gem::Version
131
117
  version: '0'
132
118
  type: :development
133
119
  prerelease: false
134
120
  version_requirements: !ruby/object:Gem::Requirement
135
121
  requirements:
136
- - - '>='
122
+ - - ">="
137
123
  - !ruby/object:Gem::Version
138
124
  version: '0'
139
125
  - !ruby/object:Gem::Dependency
140
- name: pry-debugger
126
+ name: pry
141
127
  requirement: !ruby/object:Gem::Requirement
142
128
  requirements:
143
- - - '>='
129
+ - - ">="
144
130
  - !ruby/object:Gem::Version
145
131
  version: '0'
146
132
  type: :development
147
133
  prerelease: false
148
134
  version_requirements: !ruby/object:Gem::Requirement
149
135
  requirements:
150
- - - '>='
136
+ - - ">="
151
137
  - !ruby/object:Gem::Version
152
138
  version: '0'
153
139
  description: Rescue from MySQL and Sqlite duplicate errors
154
140
  email:
155
- - guillaume@jadedpixel.com
141
+ - guillaume@shopify.com
156
142
  executables: []
157
143
  extensions: []
158
144
  extra_rdoc_files: []
159
145
  files:
160
- - .gitignore
161
- - .rspec
162
- - .travis.yml
146
+ - ".gitignore"
147
+ - ".rspec"
148
+ - ".travis.yml"
163
149
  - Gemfile
164
150
  - LICENSE.txt
165
151
  - README.md
@@ -169,10 +155,14 @@ files:
169
155
  - lib/rescue_from_duplicate/active_record.rb
170
156
  - lib/rescue_from_duplicate/active_record/extension.rb
171
157
  - lib/rescue_from_duplicate/active_record/version.rb
158
+ - lib/rescue_from_duplicate/rescuer.rb
159
+ - shipit.rubygems.yml
172
160
  - spec/gemfiles/Gemfile.ar-3.2
173
161
  - spec/gemfiles/Gemfile.ar-4.0
162
+ - spec/gemfiles/Gemfile.ar-4.1
174
163
  - spec/gemfiles/Gemfile.ar-edge
175
164
  - spec/rescue_from_duplicate_spec.rb
165
+ - spec/rescuer_spec.rb
176
166
  - spec/spec_helper.rb
177
167
  - spec/support/model.rb
178
168
  - spec/validator_spec.rb
@@ -186,17 +176,17 @@ require_paths:
186
176
  - lib
187
177
  required_ruby_version: !ruby/object:Gem::Requirement
188
178
  requirements:
189
- - - '>='
179
+ - - ">="
190
180
  - !ruby/object:Gem::Version
191
181
  version: '0'
192
182
  required_rubygems_version: !ruby/object:Gem::Requirement
193
183
  requirements:
194
- - - '>='
184
+ - - ">="
195
185
  - !ruby/object:Gem::Version
196
186
  version: '0'
197
187
  requirements: []
198
188
  rubyforge_project:
199
- rubygems_version: 2.0.14
189
+ rubygems_version: 2.2.2
200
190
  signing_key:
201
191
  specification_version: 4
202
192
  summary: Rescue from MySQL and Sqlite duplicate errors when trying to insert records
@@ -204,8 +194,10 @@ summary: Rescue from MySQL and Sqlite duplicate errors when trying to insert rec
204
194
  test_files:
205
195
  - spec/gemfiles/Gemfile.ar-3.2
206
196
  - spec/gemfiles/Gemfile.ar-4.0
197
+ - spec/gemfiles/Gemfile.ar-4.1
207
198
  - spec/gemfiles/Gemfile.ar-edge
208
199
  - spec/rescue_from_duplicate_spec.rb
200
+ - spec/rescuer_spec.rb
209
201
  - spec/spec_helper.rb
210
202
  - spec/support/model.rb
211
203
  - spec/validator_spec.rb