mcmire-shoulda-matchers 2.5.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 +7 -0
- data/.gitignore +12 -0
- data/.travis.yml +32 -0
- data/.yardopts +7 -0
- data/Appraisals +45 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +31 -0
- data/Gemfile.lock +166 -0
- data/MIT-LICENSE +22 -0
- data/NEWS.md +299 -0
- data/README.md +163 -0
- data/Rakefile +116 -0
- data/doc_config/gh-pages/index.html.erb +9 -0
- data/doc_config/yard/setup.rb +22 -0
- data/doc_config/yard/templates/default/fulldoc/html/css/bootstrap.css +5967 -0
- data/doc_config/yard/templates/default/fulldoc/html/css/full_list.css +12 -0
- data/doc_config/yard/templates/default/fulldoc/html/css/global.css +45 -0
- data/doc_config/yard/templates/default/fulldoc/html/css/solarized.css +69 -0
- data/doc_config/yard/templates/default/fulldoc/html/css/style.css +283 -0
- data/doc_config/yard/templates/default/fulldoc/html/full_list.erb +32 -0
- data/doc_config/yard/templates/default/fulldoc/html/full_list_class.erb +1 -0
- data/doc_config/yard/templates/default/fulldoc/html/full_list_method.erb +8 -0
- data/doc_config/yard/templates/default/fulldoc/html/js/app.js +300 -0
- data/doc_config/yard/templates/default/fulldoc/html/js/full_list.js +1 -0
- data/doc_config/yard/templates/default/fulldoc/html/js/jquery.stickyheaders.js +289 -0
- data/doc_config/yard/templates/default/fulldoc/html/js/underscore.min.js +6 -0
- data/doc_config/yard/templates/default/fulldoc/html/setup.rb +8 -0
- data/doc_config/yard/templates/default/layout/html/breadcrumb.erb +14 -0
- data/doc_config/yard/templates/default/layout/html/fonts.erb +1 -0
- data/doc_config/yard/templates/default/layout/html/layout.erb +23 -0
- data/doc_config/yard/templates/default/layout/html/search.erb +13 -0
- data/doc_config/yard/templates/default/layout/html/setup.rb +8 -0
- data/doc_config/yard/templates/default/method_details/html/source.erb +10 -0
- data/doc_config/yard/templates/default/module/html/box_info.erb +31 -0
- data/features/rails_integration.feature +113 -0
- data/features/step_definitions/rails_steps.rb +162 -0
- data/features/support/env.rb +5 -0
- data/gemfiles/3.0.gemfile +24 -0
- data/gemfiles/3.0.gemfile.lock +150 -0
- data/gemfiles/3.1.gemfile +27 -0
- data/gemfiles/3.1.gemfile.lock +173 -0
- data/gemfiles/3.2.gemfile +27 -0
- data/gemfiles/3.2.gemfile.lock +171 -0
- data/gemfiles/4.0.0.gemfile +28 -0
- data/gemfiles/4.0.0.gemfile.lock +172 -0
- data/gemfiles/4.0.1.gemfile +28 -0
- data/gemfiles/4.0.1.gemfile.lock +172 -0
- data/lib/shoulda-matchers.rb +1 -0
- data/lib/shoulda/matchers.rb +11 -0
- data/lib/shoulda/matchers/action_controller.rb +17 -0
- data/lib/shoulda/matchers/action_controller/filter_param_matcher.rb +64 -0
- data/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +97 -0
- data/lib/shoulda/matchers/action_controller/render_template_matcher.rb +81 -0
- data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +117 -0
- data/lib/shoulda/matchers/action_controller/rescue_from_matcher.rb +114 -0
- data/lib/shoulda/matchers/action_controller/respond_with_matcher.rb +154 -0
- data/lib/shoulda/matchers/action_controller/route_matcher.rb +116 -0
- data/lib/shoulda/matchers/action_controller/route_params.rb +48 -0
- data/lib/shoulda/matchers/action_controller/set_session_matcher.rb +164 -0
- data/lib/shoulda/matchers/action_controller/set_the_flash_matcher.rb +296 -0
- data/lib/shoulda/matchers/active_model.rb +30 -0
- data/lib/shoulda/matchers/active_model/allow_mass_assignment_of_matcher.rb +167 -0
- data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +314 -0
- data/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +46 -0
- data/lib/shoulda/matchers/active_model/ensure_exclusion_of_matcher.rb +160 -0
- data/lib/shoulda/matchers/active_model/ensure_inclusion_of_matcher.rb +417 -0
- data/lib/shoulda/matchers/active_model/ensure_length_of_matcher.rb +337 -0
- data/lib/shoulda/matchers/active_model/errors.rb +10 -0
- data/lib/shoulda/matchers/active_model/exception_message_finder.rb +58 -0
- data/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb +92 -0
- data/lib/shoulda/matchers/active_model/helpers.rb +46 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers.rb +9 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +75 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb +27 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +41 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb +27 -0
- data/lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb +26 -0
- data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +112 -0
- data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +77 -0
- data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +121 -0
- data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +380 -0
- data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +89 -0
- data/lib/shoulda/matchers/active_model/validate_uniqueness_of_matcher.rb +372 -0
- data/lib/shoulda/matchers/active_model/validation_matcher.rb +97 -0
- data/lib/shoulda/matchers/active_model/validation_message_finder.rb +69 -0
- data/lib/shoulda/matchers/active_record.rb +22 -0
- data/lib/shoulda/matchers/active_record/accept_nested_attributes_for_matcher.rb +204 -0
- data/lib/shoulda/matchers/active_record/association_matcher.rb +901 -0
- data/lib/shoulda/matchers/active_record/association_matchers.rb +9 -0
- data/lib/shoulda/matchers/active_record/association_matchers/counter_cache_matcher.rb +41 -0
- data/lib/shoulda/matchers/active_record/association_matchers/dependent_matcher.rb +41 -0
- data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +81 -0
- data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +65 -0
- data/lib/shoulda/matchers/active_record/association_matchers/option_verifier.rb +94 -0
- data/lib/shoulda/matchers/active_record/association_matchers/order_matcher.rb +41 -0
- data/lib/shoulda/matchers/active_record/association_matchers/source_matcher.rb +41 -0
- data/lib/shoulda/matchers/active_record/association_matchers/through_matcher.rb +63 -0
- data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +261 -0
- data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +149 -0
- data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +72 -0
- data/lib/shoulda/matchers/active_record/serialize_matcher.rb +181 -0
- data/lib/shoulda/matchers/assertion_error.rb +19 -0
- data/lib/shoulda/matchers/error.rb +6 -0
- data/lib/shoulda/matchers/integrations/rspec.rb +20 -0
- data/lib/shoulda/matchers/integrations/test_unit.rb +30 -0
- data/lib/shoulda/matchers/rails_shim.rb +50 -0
- data/lib/shoulda/matchers/version.rb +6 -0
- data/lib/shoulda/matchers/warn.rb +8 -0
- data/shoulda-matchers.gemspec +23 -0
- data/spec/shoulda/matchers/action_controller/filter_param_matcher_spec.rb +22 -0
- data/spec/shoulda/matchers/action_controller/redirect_to_matcher_spec.rb +42 -0
- data/spec/shoulda/matchers/action_controller/render_template_matcher_spec.rb +78 -0
- data/spec/shoulda/matchers/action_controller/render_with_layout_matcher_spec.rb +63 -0
- data/spec/shoulda/matchers/action_controller/rescue_from_matcher_spec.rb +63 -0
- data/spec/shoulda/matchers/action_controller/respond_with_matcher_spec.rb +31 -0
- data/spec/shoulda/matchers/action_controller/route_matcher_spec.rb +70 -0
- data/spec/shoulda/matchers/action_controller/route_params_spec.rb +30 -0
- data/spec/shoulda/matchers/action_controller/set_session_matcher_spec.rb +51 -0
- data/spec/shoulda/matchers/action_controller/set_the_flash_matcher_spec.rb +153 -0
- data/spec/shoulda/matchers/active_model/allow_mass_assignment_of_matcher_spec.rb +111 -0
- data/spec/shoulda/matchers/active_model/allow_value_matcher_spec.rb +170 -0
- data/spec/shoulda/matchers/active_model/disallow_value_matcher_spec.rb +81 -0
- data/spec/shoulda/matchers/active_model/ensure_exclusion_of_matcher_spec.rb +95 -0
- data/spec/shoulda/matchers/active_model/ensure_inclusion_of_matcher_spec.rb +320 -0
- data/spec/shoulda/matchers/active_model/ensure_length_of_matcher_spec.rb +166 -0
- data/spec/shoulda/matchers/active_model/exception_message_finder_spec.rb +111 -0
- data/spec/shoulda/matchers/active_model/have_secure_password_matcher_spec.rb +20 -0
- data/spec/shoulda/matchers/active_model/helpers_spec.rb +158 -0
- data/spec/shoulda/matchers/active_model/numericality_matchers/comparison_matcher_spec.rb +169 -0
- data/spec/shoulda/matchers/active_model/numericality_matchers/even_number_matcher_spec.rb +59 -0
- data/spec/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher_spec.rb +59 -0
- data/spec/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher_spec.rb +57 -0
- data/spec/shoulda/matchers/active_model/validate_absence_of_matcher_spec.rb +139 -0
- data/spec/shoulda/matchers/active_model/validate_acceptance_of_matcher_spec.rb +41 -0
- data/spec/shoulda/matchers/active_model/validate_confirmation_of_matcher_spec.rb +47 -0
- data/spec/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +331 -0
- data/spec/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +180 -0
- data/spec/shoulda/matchers/active_model/validate_uniqueness_of_matcher_spec.rb +398 -0
- data/spec/shoulda/matchers/active_model/validation_message_finder_spec.rb +127 -0
- data/spec/shoulda/matchers/active_record/accept_nested_attributes_for_matcher_spec.rb +107 -0
- data/spec/shoulda/matchers/active_record/association_matcher_spec.rb +860 -0
- data/spec/shoulda/matchers/active_record/association_matchers/model_reflection_spec.rb +247 -0
- data/spec/shoulda/matchers/active_record/have_db_column_matcher_spec.rb +111 -0
- data/spec/shoulda/matchers/active_record/have_db_index_matcher_spec.rb +78 -0
- data/spec/shoulda/matchers/active_record/have_readonly_attributes_matcher_spec.rb +41 -0
- data/spec/shoulda/matchers/active_record/serialize_matcher_spec.rb +86 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/active_model_versions.rb +13 -0
- data/spec/support/active_resource_builder.rb +29 -0
- data/spec/support/activemodel_helpers.rb +19 -0
- data/spec/support/capture_helpers.rb +19 -0
- data/spec/support/class_builder.rb +42 -0
- data/spec/support/controller_builder.rb +74 -0
- data/spec/support/fail_with_message_including_matcher.rb +33 -0
- data/spec/support/fail_with_message_matcher.rb +32 -0
- data/spec/support/i18n_faker.rb +10 -0
- data/spec/support/mailer_builder.rb +10 -0
- data/spec/support/model_builder.rb +81 -0
- data/spec/support/rails_versions.rb +18 -0
- data/spec/support/shared_examples/numerical_submatcher.rb +19 -0
- data/spec/support/shared_examples/numerical_type_submatcher.rb +17 -0
- data/spec/support/test_application.rb +120 -0
- data/yard.watchr +5 -0
- metadata +281 -0
| @@ -0,0 +1,180 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            describe Shoulda::Matchers::ActiveModel::ValidatePresenceOfMatcher do
         | 
| 4 | 
            +
              context 'a model with a presence validation' do
         | 
| 5 | 
            +
                it 'accepts' do
         | 
| 6 | 
            +
                  expect(validating_presence).to matcher
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                it 'does not override the default message with a blank' do
         | 
| 10 | 
            +
                  expect(validating_presence).to matcher.with_message(nil)
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              context 'a model without a presence validation' do
         | 
| 15 | 
            +
                it 'rejects' do
         | 
| 16 | 
            +
                  expect(define_model(:example, attr: :string).new).not_to matcher
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              context 'an ActiveModel class with a presence validation' do
         | 
| 21 | 
            +
                it 'accepts' do
         | 
| 22 | 
            +
                  expect(active_model_validating_presence).to matcher
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                it 'does not override the default message with a blank' do
         | 
| 26 | 
            +
                  expect(active_model_validating_presence).to matcher.with_message(nil)
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
              end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              context 'an ActiveModel class without a presence validation' do
         | 
| 31 | 
            +
                it 'rejects' do
         | 
| 32 | 
            +
                  expect(active_model).not_to matcher
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                it 'provides the correct failure message' do
         | 
| 36 | 
            +
                  message = %{Expected errors to include "can't be blank" when attr is set to nil, got no errors}
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  expect { expect(active_model).to matcher }.to fail_with_message(message)
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
              end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
              context 'a has_many association with a presence validation' do
         | 
| 43 | 
            +
                it 'requires the attribute to be set' do
         | 
| 44 | 
            +
                  expect(has_many_children(presence: true)).to validate_presence_of(:children)
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
              end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
              context 'a has_many association without a presence validation' do
         | 
| 49 | 
            +
                it 'does not require the attribute to be set' do
         | 
| 50 | 
            +
                  expect(has_many_children(presence: false)).
         | 
| 51 | 
            +
                    not_to validate_presence_of(:children)
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
              end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
              context 'a required has_and_belongs_to_many association' do
         | 
| 56 | 
            +
                before do
         | 
| 57 | 
            +
                  define_model :child
         | 
| 58 | 
            +
                  @model = define_model :parent do
         | 
| 59 | 
            +
                    has_and_belongs_to_many :children
         | 
| 60 | 
            +
                    validates_presence_of :children
         | 
| 61 | 
            +
                  end.new
         | 
| 62 | 
            +
                  create_table 'children_parents', id: false do |t|
         | 
| 63 | 
            +
                    t.integer :child_id
         | 
| 64 | 
            +
                    t.integer :parent_id
         | 
| 65 | 
            +
                  end
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                it 'accepts' do
         | 
| 69 | 
            +
                  expect(@model).to validate_presence_of(:children)
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
              end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
              context 'an optional has_and_belongs_to_many association' do
         | 
| 74 | 
            +
                before do
         | 
| 75 | 
            +
                  define_model :child
         | 
| 76 | 
            +
                  @model = define_model :parent do
         | 
| 77 | 
            +
                    has_and_belongs_to_many :children
         | 
| 78 | 
            +
                  end.new
         | 
| 79 | 
            +
                  create_table 'children_parents', id: false do |t|
         | 
| 80 | 
            +
                    t.integer :child_id
         | 
| 81 | 
            +
                    t.integer :parent_id
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                it 'rejects' do
         | 
| 86 | 
            +
                  expect(@model).not_to validate_presence_of(:children)
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
              end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
              context "an i18n translation containing %{attribute} and %{model}" do
         | 
| 91 | 
            +
                before do
         | 
| 92 | 
            +
                  stub_translation(
         | 
| 93 | 
            +
                    "activerecord.errors.messages.blank",
         | 
| 94 | 
            +
                    "Please enter a %{attribute} for your %{model}")
         | 
| 95 | 
            +
                end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                after { I18n.backend.reload! }
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                it "does not raise an exception" do
         | 
| 100 | 
            +
                  expect {
         | 
| 101 | 
            +
                    expect(validating_presence).to validate_presence_of(:attr)
         | 
| 102 | 
            +
                  }.to_not raise_exception
         | 
| 103 | 
            +
                end
         | 
| 104 | 
            +
              end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
              if active_model_3_2?
         | 
| 107 | 
            +
                context 'a strictly required attribute' do
         | 
| 108 | 
            +
                  it 'accepts when the :strict options match' do
         | 
| 109 | 
            +
                    expect(validating_presence(strict: true)).to matcher.strict
         | 
| 110 | 
            +
                  end
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                  it 'rejects when the :strict options do not match' do
         | 
| 113 | 
            +
                    expect(validating_presence(strict: false)).not_to matcher.strict
         | 
| 114 | 
            +
                  end
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                it 'does not override the default message with a blank' do
         | 
| 118 | 
            +
                  expect(validating_presence(strict: true)).
         | 
| 119 | 
            +
                    to matcher.strict.with_message(nil)
         | 
| 120 | 
            +
                end
         | 
| 121 | 
            +
              end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
              context "an attribute with a context-dependent validation" do
         | 
| 124 | 
            +
                context "without the validation context" do
         | 
| 125 | 
            +
                  it "does not match" do
         | 
| 126 | 
            +
                    expect(validating_presence(on: :customisable)).not_to matcher
         | 
| 127 | 
            +
                  end
         | 
| 128 | 
            +
                end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                context "with the validation context" do
         | 
| 131 | 
            +
                  it "matches" do
         | 
| 132 | 
            +
                    expect(validating_presence(on: :customisable)).to matcher.on(:customisable)
         | 
| 133 | 
            +
                  end
         | 
| 134 | 
            +
                end
         | 
| 135 | 
            +
              end
         | 
| 136 | 
            +
             | 
| 137 | 
            +
              context 'an active_resource model' do
         | 
| 138 | 
            +
                context 'with the validation context' do
         | 
| 139 | 
            +
                  it 'does not raise an exception' do
         | 
| 140 | 
            +
                    expect {
         | 
| 141 | 
            +
                      expect(active_resource_model).to validate_presence_of(:attr)
         | 
| 142 | 
            +
                    }.to_not raise_exception
         | 
| 143 | 
            +
                  end
         | 
| 144 | 
            +
                end
         | 
| 145 | 
            +
              end
         | 
| 146 | 
            +
             | 
| 147 | 
            +
              def matcher
         | 
| 148 | 
            +
                validate_presence_of(:attr)
         | 
| 149 | 
            +
              end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
              def validating_presence(options = {})
         | 
| 152 | 
            +
                define_model :example, attr: :string do
         | 
| 153 | 
            +
                  validates_presence_of :attr, options
         | 
| 154 | 
            +
                end.new
         | 
| 155 | 
            +
              end
         | 
| 156 | 
            +
             | 
| 157 | 
            +
              def active_model(&block)
         | 
| 158 | 
            +
                define_active_model_class('Example', accessors: [:attr], &block).new
         | 
| 159 | 
            +
              end
         | 
| 160 | 
            +
             | 
| 161 | 
            +
              def active_model_validating_presence
         | 
| 162 | 
            +
                active_model { validates_presence_of :attr }
         | 
| 163 | 
            +
              end
         | 
| 164 | 
            +
             | 
| 165 | 
            +
              def has_many_children(options = {})
         | 
| 166 | 
            +
                define_model :child
         | 
| 167 | 
            +
                define_model :parent do
         | 
| 168 | 
            +
                  has_many :children
         | 
| 169 | 
            +
                  if options[:presence]
         | 
| 170 | 
            +
                    validates_presence_of :children
         | 
| 171 | 
            +
                  end
         | 
| 172 | 
            +
                end.new
         | 
| 173 | 
            +
              end
         | 
| 174 | 
            +
             | 
| 175 | 
            +
              def active_resource_model
         | 
| 176 | 
            +
                define_active_resource_class :foo, attr: :string do
         | 
| 177 | 
            +
                  validates_presence_of :attr
         | 
| 178 | 
            +
                end.new
         | 
| 179 | 
            +
              end
         | 
| 180 | 
            +
            end
         | 
| @@ -0,0 +1,398 @@ | |
| 1 | 
            +
            require 'spec_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            describe Shoulda::Matchers::ActiveModel::ValidateUniquenessOfMatcher do
         | 
| 4 | 
            +
              context 'a model without a a uniqueness validation' do
         | 
| 5 | 
            +
                it 'rejects' do
         | 
| 6 | 
            +
                  model = define_model(:example, attr: :string) { attr_accessible :attr } .new
         | 
| 7 | 
            +
                  Example.create!(attr: 'value')
         | 
| 8 | 
            +
                  expect(model).not_to matcher
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              context 'a model with a uniqueness validation' do
         | 
| 13 | 
            +
                context 'where the subject has a character limit' do
         | 
| 14 | 
            +
                  it 'tests with values within the character limit' do
         | 
| 15 | 
            +
                    model = define_model(:example, attr: { type: :string, options: { limit: 1 } }) do
         | 
| 16 | 
            +
                     attr_accessible :attr
         | 
| 17 | 
            +
                     validates_uniqueness_of :attr
         | 
| 18 | 
            +
                    end.new
         | 
| 19 | 
            +
                    expect(model).to matcher
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                context 'with an existing record' do
         | 
| 24 | 
            +
                  it 'requires a unique value for that attribute' do
         | 
| 25 | 
            +
                    create_existing
         | 
| 26 | 
            +
                    expect(validating_uniqueness_with_other).to matcher
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  it 'accepts when the subject is an existing record' do
         | 
| 30 | 
            +
                    expect(create_existing).to matcher
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  it 'rejects when a scope is specified' do
         | 
| 34 | 
            +
                    create_existing
         | 
| 35 | 
            +
                    expect(validating_uniqueness_with_other).not_to matcher.scoped_to(:other)
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  def create_existing
         | 
| 39 | 
            +
                    define_model_with_other
         | 
| 40 | 
            +
                    Example.create!(attr: 'value', other: 1)
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                context 'without an existing record' do
         | 
| 45 | 
            +
                  it 'does not require a created instance' do
         | 
| 46 | 
            +
                    define_model_with_other
         | 
| 47 | 
            +
                    expect(Example.count).to eq 0
         | 
| 48 | 
            +
                    expect(validating_uniqueness_with_other).to matcher
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def define_model_with_other(options = {})
         | 
| 53 | 
            +
                  @model ||= define_model(:example, attr: :string, other: :integer) do
         | 
| 54 | 
            +
                    attr_accessible :attr, :other
         | 
| 55 | 
            +
                    validates_uniqueness_of :attr, options
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                def validating_uniqueness_with_other(options = {})
         | 
| 60 | 
            +
                  define_model_with_other.new
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
              end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
              context 'a model with a uniqueness validation, a custom error, and an existing record' do
         | 
| 65 | 
            +
                it 'rejects when the actual message does not match the default message' do
         | 
| 66 | 
            +
                  expect(validating_uniqueness_with_existing_record(message: 'Bad value')).
         | 
| 67 | 
            +
                    not_to matcher
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                it 'rejects when the messages do not match' do
         | 
| 71 | 
            +
                  expect(validating_uniqueness_with_existing_record(message: 'Bad value')).
         | 
| 72 | 
            +
                    not_to matcher.with_message(/abc/)
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                it 'accepts when the messages match' do
         | 
| 76 | 
            +
                  expect(validating_uniqueness_with_existing_record(message: 'Bad value')).
         | 
| 77 | 
            +
                    to matcher.with_message(/Bad/)
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                def validating_uniqueness_with_existing_record(options = {})
         | 
| 81 | 
            +
                  model = define_model(:example, attr: :string) do
         | 
| 82 | 
            +
                    attr_accessible :attr
         | 
| 83 | 
            +
                    validates_uniqueness_of :attr, options
         | 
| 84 | 
            +
                  end.new
         | 
| 85 | 
            +
                  Example.create!(attr: 'value')
         | 
| 86 | 
            +
                  model
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
              end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
              context 'a model with a scoped uniqueness validation with an existing value' do
         | 
| 91 | 
            +
                it 'accepts when the correct scope is specified' do
         | 
| 92 | 
            +
                  expect(validating_scoped_uniqueness([:scope1, :scope2])).
         | 
| 93 | 
            +
                    to matcher.scoped_to(:scope1, :scope2)
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                it 'accepts when the subject is an existing record' do
         | 
| 97 | 
            +
                  define_scoped_model([:scope1, :scope2])
         | 
| 98 | 
            +
                  expect(create_existing_record).to matcher.scoped_to(:scope1, :scope2)
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                it 'rejects when too narrow of a scope is specified' do
         | 
| 102 | 
            +
                  expect(validating_scoped_uniqueness([:scope1, :scope2])).
         | 
| 103 | 
            +
                    not_to matcher.scoped_to(:scope1, :scope2, :other)
         | 
| 104 | 
            +
                end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                it 'rejects when too broad of a scope is specified' do
         | 
| 107 | 
            +
                  expect(validating_scoped_uniqueness([:scope1, :scope2])).
         | 
| 108 | 
            +
                    not_to matcher.scoped_to(:scope1)
         | 
| 109 | 
            +
                end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                it 'rejects when a different scope is specified' do
         | 
| 112 | 
            +
                  expect(validating_scoped_uniqueness([:scope1])).
         | 
| 113 | 
            +
                    not_to matcher.scoped_to(:other)
         | 
| 114 | 
            +
                end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                it 'rejects when no scope is specified' do
         | 
| 117 | 
            +
                  expect(validating_scoped_uniqueness([:scope1])).not_to matcher
         | 
| 118 | 
            +
                end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                it 'rejects when a non-existent attribute is specified as a scope' do
         | 
| 121 | 
            +
                  expect(validating_scoped_uniqueness([:scope1])).
         | 
| 122 | 
            +
                    not_to matcher.scoped_to(:fake)
         | 
| 123 | 
            +
                end
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                context 'when the scoped attribute is a date' do
         | 
| 126 | 
            +
                  it "accepts" do
         | 
| 127 | 
            +
                    expect(validating_scoped_uniqueness([:scope1], :date, scope1: Date.today)).
         | 
| 128 | 
            +
                      to matcher.scoped_to(:scope1)
         | 
| 129 | 
            +
                  end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                  context 'with an existing record that conflicts with scope.next' do
         | 
| 132 | 
            +
                    it 'accepts' do
         | 
| 133 | 
            +
                      expect(validating_scoped_uniqueness_with_conflicting_next(:scope1, :date, scope1: Date.today)).
         | 
| 134 | 
            +
                        to matcher.scoped_to(:scope1)
         | 
| 135 | 
            +
                    end
         | 
| 136 | 
            +
                  end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                  context 'when too narrow of a scope is specified' do
         | 
| 139 | 
            +
                    it 'rejects' do
         | 
| 140 | 
            +
                      expect(validating_scoped_uniqueness([:scope1, :scope2], :date, scope1: Date.today, scope2: Date.today)).
         | 
| 141 | 
            +
                        not_to matcher.scoped_to(:scope1, :scope2, :other)
         | 
| 142 | 
            +
                    end
         | 
| 143 | 
            +
                  end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                  context 'when too broad of a scope is specified' do
         | 
| 146 | 
            +
                    it 'rejects' do
         | 
| 147 | 
            +
                      expect(validating_scoped_uniqueness([:scope1, :scope2], :date, scope1: Date.today, scope2: Date.today)).
         | 
| 148 | 
            +
                        not_to matcher.scoped_to(:scope1)
         | 
| 149 | 
            +
                    end
         | 
| 150 | 
            +
                  end
         | 
| 151 | 
            +
                end
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                context 'when the scoped attribute is a datetime' do
         | 
| 154 | 
            +
                  it 'accepts' do
         | 
| 155 | 
            +
                    expect(validating_scoped_uniqueness([:scope1], :datetime, scope1: DateTime.now)).
         | 
| 156 | 
            +
                      to matcher.scoped_to(:scope1)
         | 
| 157 | 
            +
                  end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                  context 'with an existing record that conflicts with scope.next' do
         | 
| 160 | 
            +
                    it 'accepts' do
         | 
| 161 | 
            +
                      expect(validating_scoped_uniqueness_with_conflicting_next(:scope1, :datetime, scope1: DateTime.now)).
         | 
| 162 | 
            +
                        to matcher.scoped_to(:scope1)
         | 
| 163 | 
            +
                    end
         | 
| 164 | 
            +
                  end
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                  context 'with a nil value' do
         | 
| 167 | 
            +
                    it 'accepts' do
         | 
| 168 | 
            +
                      expect(validating_scoped_uniqueness([:scope1], :datetime, scope1: nil)).
         | 
| 169 | 
            +
                        to matcher.scoped_to(:scope1)
         | 
| 170 | 
            +
                    end
         | 
| 171 | 
            +
                  end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                  context 'when too narrow of a scope is specified' do
         | 
| 174 | 
            +
                    it 'rejects' do
         | 
| 175 | 
            +
                      expect(validating_scoped_uniqueness([:scope1, :scope2], :datetime, scope1: DateTime.now, scope2: DateTime.now)).
         | 
| 176 | 
            +
                        not_to matcher.scoped_to(:scope1, :scope2, :other)
         | 
| 177 | 
            +
                    end
         | 
| 178 | 
            +
                  end
         | 
| 179 | 
            +
             | 
| 180 | 
            +
                  context 'when too broad of a scope is specified' do
         | 
| 181 | 
            +
                    it 'rejects' do
         | 
| 182 | 
            +
                      expect(validating_scoped_uniqueness([:scope1, :scope2], :datetime, scope1: DateTime.now, scope2: DateTime.now)).
         | 
| 183 | 
            +
                        not_to matcher.scoped_to(:scope1)
         | 
| 184 | 
            +
                    end
         | 
| 185 | 
            +
                  end
         | 
| 186 | 
            +
                end
         | 
| 187 | 
            +
             | 
| 188 | 
            +
                context 'when the scoped attribute is a uuid' do
         | 
| 189 | 
            +
                  it 'accepts' do
         | 
| 190 | 
            +
                    expect(validating_scoped_uniqueness([:scope1], :uuid, scope1: SecureRandom.uuid)).
         | 
| 191 | 
            +
                      to matcher.scoped_to(:scope1)
         | 
| 192 | 
            +
                  end
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                  context 'with an existing record that conflicts with scope.next' do
         | 
| 195 | 
            +
                    it 'accepts' do
         | 
| 196 | 
            +
                      expect(validating_scoped_uniqueness_with_conflicting_next(:scope1, :uuid, scope1: SecureRandom.uuid)).
         | 
| 197 | 
            +
                        to matcher.scoped_to(:scope1)
         | 
| 198 | 
            +
                    end
         | 
| 199 | 
            +
                  end
         | 
| 200 | 
            +
             | 
| 201 | 
            +
                  context 'with a nil value' do
         | 
| 202 | 
            +
                    it 'accepts' do
         | 
| 203 | 
            +
                      expect(validating_scoped_uniqueness([:scope1], :uuid, scope1: nil)).
         | 
| 204 | 
            +
                        to matcher.scoped_to(:scope1)
         | 
| 205 | 
            +
                    end
         | 
| 206 | 
            +
                  end
         | 
| 207 | 
            +
             | 
| 208 | 
            +
                  context 'when too narrow of a scope is specified' do
         | 
| 209 | 
            +
                    it 'rejects' do
         | 
| 210 | 
            +
                      record = validating_scoped_uniqueness([:scope1, :scope2], :uuid,
         | 
| 211 | 
            +
                        scope1: SecureRandom.uuid,
         | 
| 212 | 
            +
                        scope2: SecureRandom.uuid
         | 
| 213 | 
            +
                      )
         | 
| 214 | 
            +
                      expect(record).not_to matcher.scoped_to(:scope1, :scope2, :other)
         | 
| 215 | 
            +
                    end
         | 
| 216 | 
            +
                  end
         | 
| 217 | 
            +
             | 
| 218 | 
            +
                  context 'when too broad of a scope is specified' do
         | 
| 219 | 
            +
                    it 'rejects' do
         | 
| 220 | 
            +
                      record = validating_scoped_uniqueness([:scope1, :scope2], :uuid,
         | 
| 221 | 
            +
                        scope1: SecureRandom.uuid,
         | 
| 222 | 
            +
                        scope2: SecureRandom.uuid
         | 
| 223 | 
            +
                      )
         | 
| 224 | 
            +
                      expect(record).not_to matcher.scoped_to(:scope1)
         | 
| 225 | 
            +
                    end
         | 
| 226 | 
            +
                  end
         | 
| 227 | 
            +
                end
         | 
| 228 | 
            +
             | 
| 229 | 
            +
                def create_existing_record(attributes = {})
         | 
| 230 | 
            +
                  @existing ||= create_record(attributes)
         | 
| 231 | 
            +
                end
         | 
| 232 | 
            +
             | 
| 233 | 
            +
                def create_record(attributes = {})
         | 
| 234 | 
            +
                  default_attributes = {attr: 'value', scope1: 1, scope2: 2, other: 3}
         | 
| 235 | 
            +
                  Example.create!(default_attributes.merge(attributes))
         | 
| 236 | 
            +
                end
         | 
| 237 | 
            +
             | 
| 238 | 
            +
                def define_scoped_model(scope, scope_attr_type = :integer)
         | 
| 239 | 
            +
                  define_model(:example, attr: :string, scope1: scope_attr_type,
         | 
| 240 | 
            +
                               scope2: scope_attr_type, other: :integer) do
         | 
| 241 | 
            +
                    attr_accessible :attr, :scope1, :scope2, :other
         | 
| 242 | 
            +
                    validates_uniqueness_of :attr, scope: scope
         | 
| 243 | 
            +
                  end
         | 
| 244 | 
            +
                end
         | 
| 245 | 
            +
             | 
| 246 | 
            +
                def validating_scoped_uniqueness(*args)
         | 
| 247 | 
            +
                  attributes = args.extract_options!
         | 
| 248 | 
            +
                  model = define_scoped_model(*args).new
         | 
| 249 | 
            +
                  create_existing_record(attributes)
         | 
| 250 | 
            +
                  model
         | 
| 251 | 
            +
                end
         | 
| 252 | 
            +
             | 
| 253 | 
            +
                def validating_scoped_uniqueness_with_conflicting_next(*args)
         | 
| 254 | 
            +
                  attributes = args.extract_options!
         | 
| 255 | 
            +
                  model = define_scoped_model(*args).new
         | 
| 256 | 
            +
                  2.times do
         | 
| 257 | 
            +
                    attributes[:scope1] = attributes[:scope1].next
         | 
| 258 | 
            +
                    create_record(attributes)
         | 
| 259 | 
            +
                  end
         | 
| 260 | 
            +
                  model
         | 
| 261 | 
            +
                end
         | 
| 262 | 
            +
              end
         | 
| 263 | 
            +
             | 
| 264 | 
            +
              context 'a model with a case-sensitive uniqueness validation on a string attribute and an existing record' do
         | 
| 265 | 
            +
                it 'accepts a case-sensitive value for that attribute' do
         | 
| 266 | 
            +
                  expect(case_sensitive_validation_with_existing_value(:string)).
         | 
| 267 | 
            +
                    to matcher
         | 
| 268 | 
            +
                end
         | 
| 269 | 
            +
             | 
| 270 | 
            +
                it 'rejects a case-insensitive value for that attribute' do
         | 
| 271 | 
            +
                  expect(case_sensitive_validation_with_existing_value(:string)).
         | 
| 272 | 
            +
                    not_to matcher.case_insensitive
         | 
| 273 | 
            +
                end
         | 
| 274 | 
            +
              end
         | 
| 275 | 
            +
             | 
| 276 | 
            +
              context 'a model with a case-sensitive uniqueness validation on an integer attribute with an existing value' do
         | 
| 277 | 
            +
                it 'accepts a case-insensitive value for that attribute' do
         | 
| 278 | 
            +
                  expect(case_sensitive_validation_with_existing_value(:integer)).
         | 
| 279 | 
            +
                    to matcher.case_insensitive
         | 
| 280 | 
            +
                end
         | 
| 281 | 
            +
             | 
| 282 | 
            +
                it 'accepts a case-sensitive value for that attribute' do
         | 
| 283 | 
            +
                  expect(case_sensitive_validation_with_existing_value(:integer)).to matcher
         | 
| 284 | 
            +
                end
         | 
| 285 | 
            +
              end
         | 
| 286 | 
            +
             | 
| 287 | 
            +
              context "when the validation allows nil" do
         | 
| 288 | 
            +
                context "when there is an existing entry with a nil" do
         | 
| 289 | 
            +
                  it "should allow_nil" do
         | 
| 290 | 
            +
                    model = define_model_with_allow_nil
         | 
| 291 | 
            +
                    Example.create!(attr: nil)
         | 
| 292 | 
            +
                    expect(model).to matcher.allow_nil
         | 
| 293 | 
            +
                  end
         | 
| 294 | 
            +
                end
         | 
| 295 | 
            +
             | 
| 296 | 
            +
                if active_model_3_1?
         | 
| 297 | 
            +
                  context 'when the subject has a secure password' do
         | 
| 298 | 
            +
                    it 'allows nil on the attribute' do
         | 
| 299 | 
            +
                      model = define_model(:example, attr: :string, password_digest: :string) do |m|
         | 
| 300 | 
            +
                        validates_uniqueness_of :attr, allow_nil: true
         | 
| 301 | 
            +
                        has_secure_password
         | 
| 302 | 
            +
                      end.new
         | 
| 303 | 
            +
                      expect(model).to matcher.allow_nil
         | 
| 304 | 
            +
                    end
         | 
| 305 | 
            +
                  end
         | 
| 306 | 
            +
                end
         | 
| 307 | 
            +
             | 
| 308 | 
            +
                it "should create a nil and verify that it is allowed" do
         | 
| 309 | 
            +
                  model = define_model_with_allow_nil
         | 
| 310 | 
            +
                  expect(model).to matcher.allow_nil
         | 
| 311 | 
            +
                  Example.all.any?{ |instance| instance.attr.nil? }
         | 
| 312 | 
            +
                end
         | 
| 313 | 
            +
             | 
| 314 | 
            +
                def define_model_with_allow_nil
         | 
| 315 | 
            +
                  define_model(:example, attr: :integer) do
         | 
| 316 | 
            +
                    attr_accessible :attr
         | 
| 317 | 
            +
                    validates_uniqueness_of :attr, allow_nil: true
         | 
| 318 | 
            +
                  end.new
         | 
| 319 | 
            +
                end
         | 
| 320 | 
            +
              end
         | 
| 321 | 
            +
             | 
| 322 | 
            +
              context "when the validation does not allow a nil" do
         | 
| 323 | 
            +
                context "when there is an existing entry with a nil" do
         | 
| 324 | 
            +
                  it "should not allow_nil" do
         | 
| 325 | 
            +
                    model = define_model_without_allow_nil
         | 
| 326 | 
            +
                    Example.create!(attr: nil)
         | 
| 327 | 
            +
                    expect(model).not_to matcher.allow_nil
         | 
| 328 | 
            +
                  end
         | 
| 329 | 
            +
                end
         | 
| 330 | 
            +
             | 
| 331 | 
            +
                it "should not allow_nil" do
         | 
| 332 | 
            +
                  model = define_model_without_allow_nil
         | 
| 333 | 
            +
                  expect(model).not_to matcher.allow_nil
         | 
| 334 | 
            +
                end
         | 
| 335 | 
            +
             | 
| 336 | 
            +
                def define_model_without_allow_nil
         | 
| 337 | 
            +
                  define_model(:example, attr: :integer) do
         | 
| 338 | 
            +
                    attr_accessible :attr
         | 
| 339 | 
            +
                    validates_uniqueness_of :attr
         | 
| 340 | 
            +
                  end.new
         | 
| 341 | 
            +
                end
         | 
| 342 | 
            +
              end
         | 
| 343 | 
            +
             | 
| 344 | 
            +
              context "a model with non-nullable attribute" do
         | 
| 345 | 
            +
                context "of type" do
         | 
| 346 | 
            +
                  [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |type|
         | 
| 347 | 
            +
                    context type do
         | 
| 348 | 
            +
                      it "does not raise an error" do
         | 
| 349 | 
            +
                        model = define_model_with_non_nullable(type)
         | 
| 350 | 
            +
                        expect { expect(model).to matcher }.not_to raise_error
         | 
| 351 | 
            +
                      end
         | 
| 352 | 
            +
                    end
         | 
| 353 | 
            +
                  end
         | 
| 354 | 
            +
                end
         | 
| 355 | 
            +
             | 
| 356 | 
            +
                context "that is a primary key" do
         | 
| 357 | 
            +
                  it "does not cause duplicate entry errors by re-using default values for primary keys" do
         | 
| 358 | 
            +
                    create_table :examples, id: false do |t|
         | 
| 359 | 
            +
                      t.string :attr
         | 
| 360 | 
            +
                      t.integer :non_nullable, primary: true
         | 
| 361 | 
            +
                    end
         | 
| 362 | 
            +
                    model_class = define_model(:example, attr: :string) do
         | 
| 363 | 
            +
                      validates_uniqueness_of :attr
         | 
| 364 | 
            +
                    end
         | 
| 365 | 
            +
                    model_1 = model_class.new
         | 
| 366 | 
            +
                    model_2 = model_class.new
         | 
| 367 | 
            +
                    expect(model_1).to matcher
         | 
| 368 | 
            +
                    expect { expect(model_2).to matcher }.not_to raise_error 
         | 
| 369 | 
            +
                  end
         | 
| 370 | 
            +
                end
         | 
| 371 | 
            +
             | 
| 372 | 
            +
                def define_model_with_non_nullable(type)
         | 
| 373 | 
            +
                  define_model(:example, attr: :string, non_nullable: { type: type, options: { null: false } }) do
         | 
| 374 | 
            +
                    attr_accessible :attr, :non_nullable
         | 
| 375 | 
            +
                    validates_uniqueness_of :attr
         | 
| 376 | 
            +
                  end.new
         | 
| 377 | 
            +
                end
         | 
| 378 | 
            +
              end
         | 
| 379 | 
            +
             | 
| 380 | 
            +
              def case_sensitive_validation_with_existing_value(attr_type)
         | 
| 381 | 
            +
                model = define_model(:example, attr: attr_type) do
         | 
| 382 | 
            +
                  attr_accessible :attr
         | 
| 383 | 
            +
                  validates_uniqueness_of :attr, case_sensitive: true
         | 
| 384 | 
            +
                end.new
         | 
| 385 | 
            +
                if attr_type == :string
         | 
| 386 | 
            +
                  Example.create!(attr: 'value')
         | 
| 387 | 
            +
                elsif attr_type == :integer
         | 
| 388 | 
            +
                  Example.create!(attr: 1)
         | 
| 389 | 
            +
                else
         | 
| 390 | 
            +
                  raise 'Must be :string or :integer'
         | 
| 391 | 
            +
                end
         | 
| 392 | 
            +
                model
         | 
| 393 | 
            +
              end
         | 
| 394 | 
            +
             | 
| 395 | 
            +
              def matcher
         | 
| 396 | 
            +
                validate_uniqueness_of(:attr)
         | 
| 397 | 
            +
              end
         | 
| 398 | 
            +
            end
         |