shoulda-matchers 2.8.0 → 3.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/.hound_config/ruby.yml +7 -0
  3. data/.travis.yml +11 -54
  4. data/Appraisals +45 -100
  5. data/CONTRIBUTING.md +51 -7
  6. data/Gemfile +7 -19
  7. data/Gemfile.lock +60 -134
  8. data/Guardfile +5 -0
  9. data/NEWS.md +203 -0
  10. data/README.md +95 -50
  11. data/Rakefile +1 -0
  12. data/doc_config/yard/templates/default/layout/html/setup.rb +1 -1
  13. data/gemfiles/4.0.0.gemfile +10 -7
  14. data/gemfiles/4.0.0.gemfile.lock +103 -79
  15. data/gemfiles/4.0.1.gemfile +10 -7
  16. data/gemfiles/4.0.1.gemfile.lock +109 -83
  17. data/gemfiles/4.1.gemfile +10 -7
  18. data/gemfiles/4.1.gemfile.lock +109 -85
  19. data/gemfiles/4.2.gemfile +10 -9
  20. data/gemfiles/4.2.gemfile.lock +86 -78
  21. data/lib/shoulda/matchers.rb +13 -18
  22. data/lib/shoulda/matchers/action_controller.rb +4 -1
  23. data/lib/shoulda/matchers/action_controller/flash_store.rb +95 -0
  24. data/lib/shoulda/matchers/action_controller/{strong_parameters_matcher.rb → permit_matcher.rb} +147 -30
  25. data/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +1 -1
  26. data/lib/shoulda/matchers/action_controller/render_template_matcher.rb +1 -1
  27. data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +1 -1
  28. data/lib/shoulda/matchers/action_controller/rescue_from_matcher.rb +1 -1
  29. data/lib/shoulda/matchers/action_controller/route_matcher.rb +5 -1
  30. data/lib/shoulda/matchers/action_controller/route_params.rb +15 -6
  31. data/lib/shoulda/matchers/action_controller/session_store.rb +34 -0
  32. data/lib/shoulda/matchers/action_controller/set_flash_matcher.rb +30 -136
  33. data/lib/shoulda/matchers/action_controller/set_session_matcher.rb +28 -109
  34. data/lib/shoulda/matchers/action_controller/set_session_or_flash_matcher.rb +103 -0
  35. data/lib/shoulda/matchers/active_model/allow_mass_assignment_of_matcher.rb +1 -12
  36. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +79 -10
  37. data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +10 -0
  38. data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +21 -0
  39. data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +24 -0
  40. data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +22 -5
  41. data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +29 -10
  42. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +27 -10
  43. data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +27 -12
  44. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +56 -20
  45. data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +3 -11
  46. data/lib/shoulda/matchers/active_model/validation_message_finder.rb +65 -0
  47. data/lib/shoulda/matchers/active_record/association_matcher.rb +40 -6
  48. data/lib/shoulda/matchers/active_record/association_matchers/join_table_matcher.rb +21 -7
  49. data/lib/shoulda/matchers/active_record/association_matchers/model_reflection.rb +11 -40
  50. data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +1 -1
  51. data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +2 -6
  52. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +137 -22
  53. data/lib/shoulda/matchers/configuration.rb +20 -0
  54. data/lib/shoulda/matchers/doublespeak.rb +11 -1
  55. data/lib/shoulda/matchers/doublespeak/double.rb +29 -11
  56. data/lib/shoulda/matchers/doublespeak/double_collection.rb +4 -3
  57. data/lib/shoulda/matchers/doublespeak/method_call.rb +35 -0
  58. data/lib/shoulda/matchers/doublespeak/object_double.rb +7 -2
  59. data/lib/shoulda/matchers/doublespeak/proxy_implementation.rb +4 -3
  60. data/lib/shoulda/matchers/doublespeak/stub_implementation.rb +3 -3
  61. data/lib/shoulda/matchers/doublespeak/world.rb +21 -1
  62. data/lib/shoulda/matchers/integrations.rb +43 -0
  63. data/lib/shoulda/matchers/integrations/configuration.rb +68 -0
  64. data/lib/shoulda/matchers/integrations/configuration_error.rb +9 -0
  65. data/lib/shoulda/matchers/integrations/inclusion.rb +20 -0
  66. data/lib/shoulda/matchers/integrations/libraries.rb +15 -0
  67. data/lib/shoulda/matchers/integrations/libraries/action_controller.rb +31 -0
  68. data/lib/shoulda/matchers/integrations/libraries/active_model.rb +26 -0
  69. data/lib/shoulda/matchers/integrations/libraries/active_record.rb +26 -0
  70. data/lib/shoulda/matchers/integrations/libraries/missing_library.rb +19 -0
  71. data/lib/shoulda/matchers/integrations/libraries/rails.rb +30 -0
  72. data/lib/shoulda/matchers/integrations/rails.rb +12 -0
  73. data/lib/shoulda/matchers/integrations/registry.rb +28 -0
  74. data/lib/shoulda/matchers/integrations/test_frameworks.rb +16 -0
  75. data/lib/shoulda/matchers/integrations/test_frameworks/active_support_test_case.rb +37 -0
  76. data/lib/shoulda/matchers/integrations/test_frameworks/minitest_4.rb +36 -0
  77. data/lib/shoulda/matchers/integrations/test_frameworks/minitest_5.rb +37 -0
  78. data/lib/shoulda/matchers/integrations/test_frameworks/missing_test_framework.rb +40 -0
  79. data/lib/shoulda/matchers/integrations/test_frameworks/rspec.rb +29 -0
  80. data/lib/shoulda/matchers/integrations/test_frameworks/test_unit.rb +36 -0
  81. data/lib/shoulda/matchers/rails_shim.rb +0 -40
  82. data/lib/shoulda/matchers/version.rb +1 -1
  83. data/script/SUPPORTED_VERSIONS +1 -1
  84. data/script/update_gems_in_all_appraisals +14 -0
  85. data/shoulda-matchers.gemspec +2 -2
  86. data/spec/acceptance/active_model_integration_spec.rb +4 -1
  87. data/spec/acceptance/independent_matchers_spec.rb +6 -6
  88. data/spec/acceptance/multiple_libraries_integration_spec.rb +52 -0
  89. data/spec/acceptance/rails_integration_spec.rb +15 -5
  90. data/spec/acceptance_spec_helper.rb +8 -0
  91. data/spec/doublespeak_spec_helper.rb +14 -0
  92. data/spec/support/acceptance/adds_shoulda_matchers_to_project.rb +110 -0
  93. data/spec/support/acceptance/helpers.rb +2 -0
  94. data/spec/support/acceptance/helpers/base_helpers.rb +6 -1
  95. data/spec/support/acceptance/helpers/command_helpers.rb +6 -2
  96. data/spec/support/acceptance/helpers/minitest_helpers.rb +0 -8
  97. data/spec/support/acceptance/helpers/n_unit_helpers.rb +25 -0
  98. data/spec/support/acceptance/helpers/rspec_helpers.rb +2 -0
  99. data/spec/support/acceptance/helpers/step_helpers.rb +13 -19
  100. data/spec/support/acceptance/matchers/have_output.rb +1 -1
  101. data/spec/support/tests/bundle.rb +1 -1
  102. data/spec/support/tests/command_runner.rb +25 -13
  103. data/spec/support/tests/current_bundle.rb +47 -0
  104. data/spec/support/tests/database.rb +28 -0
  105. data/spec/support/tests/database_adapters/postgresql.rb +25 -0
  106. data/spec/support/tests/database_adapters/sqlite3.rb +26 -0
  107. data/spec/support/tests/database_configuration.rb +33 -0
  108. data/spec/support/tests/database_configuration_registry.rb +28 -0
  109. data/spec/support/tests/filesystem.rb +25 -2
  110. data/spec/support/unit/helpers/active_record_versions.rb +12 -0
  111. data/spec/support/unit/helpers/class_builder.rb +6 -2
  112. data/spec/support/unit/helpers/column_type_helpers.rb +26 -0
  113. data/spec/support/unit/helpers/controller_builder.rb +0 -28
  114. data/spec/support/unit/helpers/database_helpers.rb +18 -0
  115. data/spec/support/unit/helpers/model_builder.rb +38 -6
  116. data/spec/support/unit/helpers/rails_versions.rb +2 -2
  117. data/spec/support/unit/matchers/fail_with_message_including_matcher.rb +9 -8
  118. data/spec/support/unit/matchers/fail_with_message_matcher.rb +1 -1
  119. data/spec/support/unit/rails_application.rb +29 -13
  120. data/spec/support/unit/record_validating_confirmation_builder.rb +1 -2
  121. data/spec/support/unit/shared_examples/set_session_or_flash.rb +355 -0
  122. data/spec/unit/shoulda/matchers/action_controller/permit_matcher_spec.rb +433 -0
  123. data/spec/unit/shoulda/matchers/action_controller/render_with_layout_matcher_spec.rb +1 -5
  124. data/spec/unit/shoulda/matchers/action_controller/route_matcher_spec.rb +37 -0
  125. data/spec/unit/shoulda/matchers/action_controller/set_flash_matcher_spec.rb +23 -147
  126. data/spec/unit/shoulda/matchers/action_controller/set_session_matcher_spec.rb +8 -285
  127. data/spec/unit/shoulda/matchers/action_controller/set_session_or_flash_matcher_spec.rb +562 -0
  128. data/spec/unit/shoulda/matchers/active_model/allow_value_matcher_spec.rb +81 -14
  129. data/spec/unit/shoulda/matchers/active_model/disallow_value_matcher_spec.rb +16 -8
  130. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/comparison_matcher_spec.rb +101 -9
  131. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/even_number_matcher_spec.rb +39 -1
  132. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher_spec.rb +39 -1
  133. data/spec/unit/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher_spec.rb +39 -0
  134. data/spec/unit/shoulda/matchers/active_model/validate_exclusion_of_matcher_spec.rb +0 -17
  135. data/spec/unit/shoulda/matchers/active_model/validate_inclusion_of_matcher_spec.rb +0 -17
  136. data/spec/unit/shoulda/matchers/active_model/validate_length_of_matcher_spec.rb +0 -17
  137. data/spec/unit/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +838 -271
  138. data/spec/unit/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +0 -19
  139. data/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb +93 -0
  140. data/spec/unit/shoulda/matchers/active_record/association_matchers/model_reflection_spec.rb +3 -3
  141. data/spec/unit/shoulda/matchers/active_record/define_enum_for_matcher_spec.rb +25 -0
  142. data/spec/unit/shoulda/matchers/active_record/validate_uniqueness_of_matcher_spec.rb +905 -0
  143. data/spec/unit/shoulda/matchers/doublespeak/double_collection_spec.rb +17 -11
  144. data/spec/unit/shoulda/matchers/doublespeak/double_implementation_registry_spec.rb +1 -1
  145. data/spec/unit/shoulda/matchers/doublespeak/double_spec.rb +144 -43
  146. data/spec/unit/shoulda/matchers/doublespeak/object_double_spec.rb +1 -1
  147. data/spec/unit/shoulda/matchers/doublespeak/proxy_implementation_spec.rb +36 -11
  148. data/spec/unit/shoulda/matchers/doublespeak/stub_implementation_spec.rb +29 -16
  149. data/spec/unit/shoulda/matchers/doublespeak/world_spec.rb +8 -5
  150. data/spec/unit/shoulda/matchers/doublespeak_spec.rb +1 -1
  151. data/spec/unit_spec_helper.rb +15 -14
  152. data/spec/warnings_spy.rb +1 -1
  153. metadata +68 -29
  154. data/docs.watchr +0 -5
  155. data/gemfiles/3.0.gemfile +0 -26
  156. data/gemfiles/3.0.gemfile.lock +0 -173
  157. data/gemfiles/3.1.gemfile +0 -32
  158. data/gemfiles/3.1.gemfile.lock +0 -212
  159. data/gemfiles/3.1_1.9.2.gemfile +0 -32
  160. data/gemfiles/3.1_1.9.2.gemfile.lock +0 -212
  161. data/gemfiles/3.2.gemfile +0 -33
  162. data/gemfiles/3.2.gemfile.lock +0 -212
  163. data/gemfiles/3.2_1.9.2.gemfile +0 -31
  164. data/gemfiles/3.2_1.9.2.gemfile.lock +0 -207
  165. data/lib/shoulda/matchers/assertion_error.rb +0 -27
  166. data/lib/shoulda/matchers/doublespeak/structs.rb +0 -10
  167. data/lib/shoulda/matchers/integrations/nunit_test_case_detection.rb +0 -39
  168. data/lib/shoulda/matchers/integrations/rspec.rb +0 -19
  169. data/lib/shoulda/matchers/integrations/test_unit.rb +0 -34
  170. data/spec/unit/shoulda/matchers/action_controller/strong_parameters_matcher_spec.rb +0 -331
  171. data/spec/unit/shoulda/matchers/active_model/validate_uniqueness_of_matcher_spec.rb +0 -564
@@ -163,25 +163,6 @@ describe Shoulda::Matchers::ActiveModel::ValidatePresenceOfMatcher, type: :model
163
163
  end
164
164
  end
165
165
 
166
- context 'when the attribute being tested intercepts the blank value we set on it (issue #479)' do
167
- context 'for a non-collection attribute' do
168
- it 'does not raise an error' do
169
- record = define_model :example, attr: :string do
170
- validates :attr, presence: true
171
-
172
- def attr=(value)
173
- value = '' if value.nil?
174
- super(value)
175
- end
176
- end.new
177
-
178
- expect do
179
- expect(record).to validate_presence_of(:attr)
180
- end.not_to raise_error
181
- end
182
- end
183
- end
184
-
185
166
  def matcher
186
167
  validate_presence_of(:attr)
187
168
  end
@@ -886,6 +886,99 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatcher, type: :model do
886
886
  end.to fail_with_message_including('missing columns: person_id, relative_id')
887
887
  end
888
888
 
889
+ context 'when the association is declared with a :join_table option' do
890
+ it 'accepts when testing with the same :join_table option' do
891
+ join_table_name = 'people_and_their_families'
892
+
893
+ define_model :relative
894
+
895
+ define_model :person do
896
+ has_and_belongs_to_many(:relatives, join_table: join_table_name)
897
+ end
898
+
899
+ create_table(join_table_name, id: false) do |t|
900
+ t.references :person
901
+ t.references :relative
902
+ end
903
+
904
+ expect(Person.new).
905
+ to have_and_belong_to_many(:relatives).
906
+ join_table(join_table_name)
907
+ end
908
+
909
+ it 'accepts even when not explicitly testing with a :join_table option' do
910
+ join_table_name = 'people_and_their_families'
911
+
912
+ define_model :relative
913
+
914
+ define_model :person do
915
+ has_and_belongs_to_many(:relatives,
916
+ join_table: join_table_name
917
+ )
918
+ end
919
+
920
+ create_table(join_table_name, id: false) do |t|
921
+ t.references :person
922
+ t.references :relative
923
+ end
924
+
925
+ expect(Person.new).to have_and_belong_to_many(:relatives)
926
+ end
927
+
928
+ it 'rejects when testing with a different :join_table option' do
929
+ join_table_name = 'people_and_their_families'
930
+
931
+ define_model :relative
932
+
933
+ define_model :person do
934
+ has_and_belongs_to_many(
935
+ :relatives,
936
+ join_table: join_table_name
937
+ )
938
+ end
939
+
940
+ create_table(join_table_name, id: false) do |t|
941
+ t.references :person
942
+ t.references :relative
943
+ end
944
+
945
+ assertion = lambda do
946
+ expect(Person.new).
947
+ to have_and_belong_to_many(:relatives).
948
+ join_table('family_tree')
949
+ end
950
+
951
+ expect(&assertion).to fail_with_message_including(
952
+ "relatives should use 'family_tree' for :join_table option"
953
+ )
954
+ end
955
+ end
956
+
957
+ context 'when the association is not declared with a :join_table option' do
958
+ it 'rejects when testing with a :join_table option' do
959
+ define_model :relative
960
+
961
+ define_model :person do
962
+ has_and_belongs_to_many(:relatives)
963
+ end
964
+
965
+ create_table('people_relatives', id: false) do |t|
966
+ t.references :person
967
+ t.references :relative
968
+ end
969
+
970
+ assertion = lambda do
971
+ expect(Person.new).
972
+ to have_and_belong_to_many(:relatives).
973
+ join_table('family_tree')
974
+ end
975
+
976
+ expect(&assertion).to fail_with_message_including(
977
+ "relatives should use 'family_tree' for :join_table option"
978
+ )
979
+ end
980
+ end
981
+
889
982
  context 'using a custom foreign key' do
890
983
  it 'rejects an association with a join table with incorrect columns' do
891
984
  define_model :relative
@@ -54,7 +54,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatchers::ModelReflection d
54
54
  end
55
55
  end
56
56
 
57
- describe '#join_table' do
57
+ describe '#join_table_name' do
58
58
  context 'when the association was defined with a :join_table option' do
59
59
  it 'returns the value of the option' do
60
60
  create_table :foos, id: false do |t|
@@ -68,7 +68,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatchers::ModelReflection d
68
68
  delegate_reflection = country_model.reflect_on_association(:people)
69
69
  reflection = described_class.new(delegate_reflection)
70
70
 
71
- expect(reflection.join_table).to eq 'foos'
71
+ expect(reflection.join_table_name).to eq 'foos'
72
72
  end
73
73
  end
74
74
 
@@ -81,7 +81,7 @@ describe Shoulda::Matchers::ActiveRecord::AssociationMatchers::ModelReflection d
81
81
  delegate_reflection = country_model.reflect_on_association(:people)
82
82
  reflection = described_class.new(delegate_reflection)
83
83
 
84
- expect(reflection.join_table).to eq 'countries_people'
84
+ expect(reflection.join_table_name).to eq 'countries_people'
85
85
  end
86
86
  end
87
87
  end
@@ -2,6 +2,31 @@ require "unit_spec_helper"
2
2
 
3
3
  describe Shoulda::Matchers::ActiveRecord::DefineEnumForMatcher, type: :model do
4
4
  if active_record_supports_enum?
5
+ context 'if the attribute is given in plural form accidentally' do
6
+ it 'rejects' do
7
+ record = record_with_array_values
8
+ plural_enum_attribute = enum_attribute.to_s.pluralize
9
+ message = "Expected #{record.class} to define :#{plural_enum_attribute} as an enum"
10
+ assertion = -> {
11
+ expect(record).to define_enum_for(plural_enum_attribute)
12
+ }
13
+ expect(&assertion).to fail_with_message(message)
14
+ end
15
+ end
16
+
17
+ context 'if a method to hold enum values exists on the model but was not created via the enum macro' do
18
+ it 'rejects' do
19
+ model = define_model :example do
20
+ def self.statuses; end
21
+ end
22
+ message = "Expected #{model} to define :statuses as an enum"
23
+ assertion = -> {
24
+ expect(model.new).to define_enum_for(:statuses)
25
+ }
26
+ expect(&assertion).to fail_with_message(message)
27
+ end
28
+ end
29
+
5
30
  describe "with only the attribute name specified" do
6
31
  it "accepts a record where the attribute is defined as an enum" do
7
32
  expect(record_with_array_values).to define_enum_for(enum_attribute)
@@ -0,0 +1,905 @@
1
+ require 'unit_spec_helper'
2
+
3
+ describe Shoulda::Matchers::ActiveRecord::ValidateUniquenessOfMatcher, type: :model do
4
+ shared_context 'it supports scoped attributes of a certain type' do |options = {}|
5
+ column_type = options.fetch(:column_type)
6
+ value_type = options.fetch(:value_type, column_type)
7
+ array = options.fetch(:array, false)
8
+
9
+ context 'when the correct scope is specified' do
10
+ context 'when the subject is a new record' do
11
+ it 'accepts' do
12
+ record = build_record_validating_uniqueness(
13
+ scopes: [
14
+ build_attribute(name: :scope1),
15
+ { name: :scope2 }
16
+ ]
17
+ )
18
+ expect(record).to validate_uniqueness.scoped_to(:scope1, :scope2)
19
+ end
20
+
21
+ it 'still accepts if the scope is unset beforehand' do
22
+ record = build_record_validating_uniqueness(
23
+ scopes: [ build_attribute(name: :scope, value: nil) ]
24
+ )
25
+
26
+ expect(record).to validate_uniqueness.scoped_to(:scope)
27
+ end
28
+ end
29
+
30
+ context 'when the subject is an existing record' do
31
+ it 'accepts' do
32
+ record = create_record_validating_uniqueness(
33
+ scopes: [
34
+ build_attribute(name: :scope1),
35
+ { name: :scope2 }
36
+ ]
37
+ )
38
+
39
+ expect(record).to validate_uniqueness.scoped_to(:scope1, :scope2)
40
+ end
41
+
42
+ it 'still accepts if the scope is unset beforehand' do
43
+ record = create_record_validating_uniqueness(
44
+ scopes: [ build_attribute(name: :scope, value: nil) ]
45
+ )
46
+
47
+ expect(record).to validate_uniqueness.scoped_to(:scope)
48
+ end
49
+ end
50
+ end
51
+
52
+ context "when more than one record exists that has the next version of the attribute's value" do
53
+ it 'accepts' do
54
+ value1 = dummy_value_for(value_type, array: array)
55
+ value2 = next_version_of(value1, value_type)
56
+ value3 = next_version_of(value2, value_type)
57
+ model = define_model_validating_uniqueness(
58
+ scopes: [ build_attribute(name: :scope) ]
59
+ )
60
+ create_record_from(model, scope: value2)
61
+ create_record_from(model, scope: value3)
62
+ record = build_record_from(model, scope: value1)
63
+
64
+ expect(record).to validate_uniqueness.scoped_to(:scope)
65
+ end
66
+ end
67
+
68
+ context 'when too narrow of a scope is specified' do
69
+ it 'rejects' do
70
+ record = build_record_validating_uniqueness(
71
+ scopes: [
72
+ build_attribute(name: :scope1),
73
+ { name: :scope2 }
74
+ ],
75
+ )
76
+ expect(record).
77
+ not_to validate_uniqueness.
78
+ scoped_to(:scope1, :scope2, :other)
79
+ end
80
+ end
81
+
82
+ context 'when too broad of a scope is specified' do
83
+ it 'rejects' do
84
+ record = build_record_validating_uniqueness(
85
+ scopes: [
86
+ build_attribute(name: :scope1),
87
+ { name: :scope2 }
88
+ ],
89
+ )
90
+ expect(record).
91
+ not_to validate_uniqueness.
92
+ scoped_to(:scope1)
93
+ end
94
+ end
95
+
96
+ context 'when a different scope is specified' do
97
+ it 'rejects' do
98
+ record = build_record_validating_uniqueness(
99
+ scopes: [ build_attribute(name: :scope) ],
100
+ additional_attributes: [:other]
101
+ )
102
+ expect(record).
103
+ not_to validate_uniqueness.
104
+ scoped_to(:other)
105
+ end
106
+ end
107
+
108
+ context 'when no scope is specified' do
109
+ it 'rejects' do
110
+ record = build_record_validating_uniqueness(
111
+ scopes: [ build_attribute(name: :scope) ]
112
+ )
113
+ expect(record).not_to validate_uniqueness
114
+ end
115
+
116
+ it 'rejects if the scope is unset beforehand' do
117
+ record = build_record_validating_uniqueness(
118
+ scopes: [ build_attribute(name: :scope, value: nil) ]
119
+ )
120
+ expect(record).not_to validate_uniqueness
121
+ end
122
+ end
123
+
124
+ context 'when a non-existent attribute is specified as a scope' do
125
+ it 'rejects' do
126
+ record = build_record_validating_uniqueness(
127
+ scopes: [ build_attribute(name: :scope) ]
128
+ )
129
+ expect(record).not_to validate_uniqueness.scoped_to(:non_existent)
130
+ end
131
+ end
132
+
133
+ define_method(:build_attribute) do |attribute_options|
134
+ attribute_options.merge(
135
+ column_type: column_type,
136
+ value_type: value_type,
137
+ array: array
138
+ )
139
+ end
140
+ end
141
+
142
+ context 'when the model does not have a uniqueness validation' do
143
+ it 'rejects' do
144
+ model = define_model(:example, attribute_name => :string) do |m|
145
+ m.attr_accessible attribute_name
146
+ end
147
+
148
+ model.create!(attr: 'value')
149
+
150
+ expect(model.new).not_to validate_uniqueness_of(attribute_name)
151
+ end
152
+ end
153
+
154
+ context 'when the model has a uniqueness validation' do
155
+ context 'when the attribute has a character limit' do
156
+ it 'accepts' do
157
+ record = build_record_validating_uniqueness(
158
+ attribute_type: :string,
159
+ attribute_options: { limit: 1 }
160
+ )
161
+
162
+ expect(record).to validate_uniqueness
163
+ end
164
+ end
165
+
166
+ context 'when the record is created beforehand' do
167
+ context 'when the subject is a new record' do
168
+ it 'accepts' do
169
+ create_record_validating_uniqueness
170
+ expect(new_record_validating_uniqueness).
171
+ to validate_uniqueness
172
+ end
173
+ end
174
+
175
+ context 'when the subject is an existing record' do
176
+ it 'accepts' do
177
+ expect(existing_record_validating_uniqueness).to validate_uniqueness
178
+ end
179
+ end
180
+
181
+ context 'when the validation has no scope and a scope is specified' do
182
+ it 'rejects' do
183
+ model = define_model_validating_uniqueness(
184
+ additional_attributes: [:other]
185
+ )
186
+ create_record_from(model)
187
+ record = build_record_from(model)
188
+ expect(record).not_to validate_uniqueness.scoped_to(:other)
189
+ end
190
+ end
191
+ end
192
+
193
+ context 'when the record is not created beforehand' do
194
+ it 'creates the record automatically' do
195
+ model = define_model_validating_uniqueness
196
+ assertion = -> {
197
+ record = build_record_from(model)
198
+ expect(record).to validate_uniqueness
199
+ }
200
+ expect(&assertion).to change(model, :count).from(0).to(1)
201
+ end
202
+
203
+ context 'and the table has required attributes other than the attribute being validated, set beforehand' do
204
+ it 'does not require the record to be persisted' do
205
+ options = {
206
+ additional_attributes: [
207
+ { name: :required_attribute, options: { null: false } }
208
+ ]
209
+ }
210
+ model = define_model_validating_uniqueness(options) do |m|
211
+ m.validates_presence_of :required_attribute
212
+ end
213
+
214
+ record = build_record_from(model, required_attribute: 'something')
215
+ expect(record).to validate_uniqueness
216
+ end
217
+ end
218
+ end
219
+
220
+ context 'and the validation has a custom message' do
221
+ context 'when no message is specified' do
222
+ it 'rejects' do
223
+ record = build_record_validating_uniqueness(
224
+ validation_options: { message: 'bad value' }
225
+ )
226
+ expect(record).not_to validate_uniqueness
227
+ end
228
+ end
229
+
230
+ context 'given a string' do
231
+ context 'when the given and actual messages do not match' do
232
+ it 'rejects' do
233
+ record = build_record_validating_uniqueness(
234
+ validation_options: { message: 'bad value' }
235
+ )
236
+ expect(record).
237
+ not_to validate_uniqueness.
238
+ with_message('something else entirely')
239
+ end
240
+ end
241
+
242
+ context 'when the given and actual messages match' do
243
+ it 'accepts' do
244
+ record = build_record_validating_uniqueness(
245
+ validation_options: { message: 'bad value' }
246
+ )
247
+ expect(record).
248
+ to validate_uniqueness.
249
+ with_message('bad value')
250
+ end
251
+ end
252
+ end
253
+
254
+ context 'given a regex' do
255
+ context 'when the given and actual messages do not match' do
256
+ it 'rejects' do
257
+ record = build_record_validating_uniqueness(
258
+ validation_options: { message: 'Bad value' }
259
+ )
260
+ expect(record).
261
+ not_to validate_uniqueness.
262
+ with_message(/something else entirely/)
263
+ end
264
+ end
265
+
266
+ context 'when the given and actual messages match' do
267
+ it 'accepts' do
268
+ record = build_record_validating_uniqueness(
269
+ validation_options: { message: 'bad value' }
270
+ )
271
+ expect(record).
272
+ to validate_uniqueness.
273
+ with_message(/bad/)
274
+ end
275
+ end
276
+ end
277
+ end
278
+ end
279
+
280
+ context 'when the model has a scoped uniqueness validation' do
281
+ context 'when one of the scoped attributes is a string column' do
282
+ include_context 'it supports scoped attributes of a certain type',
283
+ column_type: :string
284
+ end
285
+
286
+ context 'when one of the scoped attributes is a boolean column' do
287
+ include_context 'it supports scoped attributes of a certain type',
288
+ column_type: :boolean
289
+ end
290
+
291
+ context 'when there is more than one scoped attribute and all are boolean columns' do
292
+ it 'accepts when all of the scoped attributes are true' do
293
+ record = build_record_validating_uniqueness(
294
+ scopes: [
295
+ { type: :boolean, name: :scope1, value: true },
296
+ { type: :boolean, name: :scope2, value: true }
297
+ ]
298
+ )
299
+ expect(record).to validate_uniqueness.scoped_to(:scope1, :scope2)
300
+ end
301
+
302
+ it 'accepts when all the scoped attributes are false' do
303
+ record = build_record_validating_uniqueness(
304
+ scopes: [
305
+ { type: :boolean, name: :scope1, value: false },
306
+ { type: :boolean, name: :scope2, value: false }
307
+ ]
308
+ )
309
+ expect(record).to validate_uniqueness.scoped_to(:scope1, :scope2)
310
+ end
311
+
312
+ it 'accepts when one of the scoped attributes is true and the other is false' do
313
+ record = build_record_validating_uniqueness(
314
+ scopes: [
315
+ { type: :boolean, name: :scope1, value: true },
316
+ { type: :boolean, name: :scope2, value: false }
317
+ ]
318
+ )
319
+ expect(record).to validate_uniqueness.scoped_to(:scope1, :scope2)
320
+ end
321
+ end
322
+
323
+ context 'when one of the scoped attributes is an integer column' do
324
+ include_context 'it supports scoped attributes of a certain type',
325
+ column_type: :integer
326
+
327
+ if active_record_supports_enum?
328
+ context 'when one of the scoped attributes is an enum' do
329
+ it 'accepts' do
330
+ record = build_record_validating_scoped_uniqueness_with_enum(
331
+ enum_scope: :scope
332
+ )
333
+ expect(record).to validate_uniqueness.scoped_to(:scope)
334
+ end
335
+
336
+ context 'when too narrow of a scope is specified' do
337
+ it 'rejects' do
338
+ record = build_record_validating_scoped_uniqueness_with_enum(
339
+ enum_scope: :scope1,
340
+ additional_scopes: [:scope2],
341
+ additional_attributes: [:other]
342
+ )
343
+ expect(record).
344
+ not_to validate_uniqueness.
345
+ scoped_to(:scope1, :scope2, :other)
346
+ end
347
+ end
348
+
349
+ context 'when too broad of a scope is specified' do
350
+ it 'rejects' do
351
+ record = build_record_validating_scoped_uniqueness_with_enum(
352
+ enum_scope: :scope1,
353
+ additional_scopes: [:scope2]
354
+ )
355
+ expect(record).
356
+ not_to validate_uniqueness.
357
+ scoped_to(:scope1)
358
+ end
359
+ end
360
+ end
361
+ end
362
+ end
363
+
364
+ context 'when one of the scoped attributes is a date column' do
365
+ include_context 'it supports scoped attributes of a certain type',
366
+ column_type: :date
367
+ end
368
+
369
+ context 'when one of the scoped attributes is a datetime column (using DateTime)' do
370
+ include_context 'it supports scoped attributes of a certain type',
371
+ column_type: :datetime
372
+ end
373
+
374
+ context 'when one of the scoped attributes is a datetime column (using Time)' do
375
+ include_context 'it supports scoped attributes of a certain type',
376
+ column_type: :datetime,
377
+ value_type: :time
378
+ end
379
+
380
+ context 'when one of the scoped attributes is a text column' do
381
+ include_context 'it supports scoped attributes of a certain type',
382
+ column_type: :text
383
+ end
384
+
385
+ if database_supports_uuid_columns?
386
+ context 'when one of the scoped attributes is a UUID column' do
387
+ include_context 'it supports scoped attributes of a certain type',
388
+ column_type: :uuid
389
+ end
390
+ end
391
+
392
+ if database_supports_array_columns? && active_record_supports_array_columns?
393
+ context 'when one of the scoped attributes is a array-of-string column' do
394
+ include_examples 'it supports scoped attributes of a certain type',
395
+ column_type: :string,
396
+ array: true
397
+ end
398
+
399
+ context 'when one of the scoped attributes is an array-of-integer column' do
400
+ include_examples 'it supports scoped attributes of a certain type',
401
+ column_type: :integer,
402
+ array: true
403
+ end
404
+
405
+ context 'when one of the scoped attributes is an array-of-date column' do
406
+ include_examples 'it supports scoped attributes of a certain type',
407
+ column_type: :date,
408
+ array: true
409
+ end
410
+
411
+ context 'when one of the scoped attributes is an array-of-datetime column (using DateTime)' do
412
+ include_examples 'it supports scoped attributes of a certain type',
413
+ column_type: :datetime,
414
+ array: true
415
+ end
416
+
417
+ context 'when one of the scoped attributes is an array-of-datetime column (using Time)' do
418
+ include_examples 'it supports scoped attributes of a certain type',
419
+ column_type: :datetime,
420
+ value_type: :time,
421
+ array: true
422
+ end
423
+
424
+ context 'when one of the scoped attributes is an array-of-text column' do
425
+ include_examples 'it supports scoped attributes of a certain type',
426
+ column_type: :text,
427
+ array: true
428
+ end
429
+ end
430
+
431
+ context "when an existing record that is not the first has a nil value for the scoped attribute" do
432
+ it 'still works' do
433
+ model = define_model_validating_uniqueness(scopes: [:scope])
434
+ create_record_from(model, scope: 'some value')
435
+ create_record_from(model, scope: nil)
436
+ record = build_record_from(model, scope: 'a different value')
437
+
438
+ expect(record).to validate_uniqueness.scoped_to(:scope)
439
+ end
440
+ end
441
+ end
442
+
443
+ context 'when the model has a case-sensitive validation' do
444
+ context 'when case_insensitive is not specified' do
445
+ it 'accepts' do
446
+ record = build_record_validating_uniqueness(
447
+ attribute_type: :string,
448
+ validation_options: { case_sensitive: true }
449
+ )
450
+
451
+ expect(record).to validate_uniqueness
452
+ end
453
+ end
454
+
455
+ context 'when case_insensitive is specified' do
456
+ it 'rejects' do
457
+ record = build_record_validating_uniqueness(
458
+ attribute_type: :string,
459
+ validation_options: { case_sensitive: true }
460
+ )
461
+
462
+ expect(record).not_to validate_uniqueness.case_insensitive
463
+ end
464
+ end
465
+ end
466
+
467
+ context 'when the model has a case-insensitive validation' do
468
+ context 'when case_insensitive is not specified' do
469
+ it 'rejects' do
470
+ record = build_record_validating_uniqueness(
471
+ attribute_type: :string,
472
+ validation_options: { case_sensitive: false }
473
+ )
474
+
475
+ expect(record).not_to validate_uniqueness
476
+ end
477
+ end
478
+
479
+ context 'when case_insensitive is specified' do
480
+ it 'accepts' do
481
+ record = build_record_validating_uniqueness(
482
+ attribute_type: :string,
483
+ validation_options: { case_sensitive: false }
484
+ )
485
+
486
+ expect(record).to validate_uniqueness.case_insensitive
487
+ end
488
+ end
489
+ end
490
+
491
+ context 'when the validation is declared with allow_nil' do
492
+ context 'given a new record whose attribute is nil' do
493
+ it 'accepts' do
494
+ model = define_model_validating_uniqueness(
495
+ validation_options: { allow_nil: true }
496
+ )
497
+ record = build_record_from(model, attribute_name => nil)
498
+ expect(record).to validate_uniqueness.allow_nil
499
+ end
500
+ end
501
+
502
+ context 'given an existing record whose attribute is nil' do
503
+ it 'accepts' do
504
+ model = define_model_validating_uniqueness(
505
+ validation_options: { allow_nil: true }
506
+ )
507
+ record = create_record_from(model, attribute_name => nil)
508
+ expect(record).to validate_uniqueness.allow_nil
509
+ end
510
+ end
511
+
512
+ if active_record_supports_has_secure_password?
513
+ context 'when the model is declared with has_secure_password' do
514
+ it 'accepts' do
515
+ model = define_model_validating_uniqueness(
516
+ validation_options: { allow_nil: true },
517
+ additional_attributes: [{ name: :password_digest, type: :string }]
518
+ ) do |m|
519
+ m.has_secure_password
520
+ end
521
+
522
+ record = build_record_from(model, attribute_name => nil)
523
+
524
+ expect(record).to validate_uniqueness.allow_nil
525
+ end
526
+ end
527
+ end
528
+ end
529
+
530
+ context 'when the validation is not declared with allow_nil' do
531
+ context 'given a new record whose attribute is nil' do
532
+ it 'rejects' do
533
+ model = define_model_validating_uniqueness
534
+ record = build_record_from(model, attribute_name => nil)
535
+ expect(record).not_to validate_uniqueness.allow_nil
536
+ end
537
+ end
538
+
539
+ context 'given an existing record whose attribute is nil' do
540
+ it 'rejects' do
541
+ model = define_model_validating_uniqueness
542
+ record = create_record_from(model, attribute_name => nil)
543
+ expect(record).not_to validate_uniqueness.allow_nil
544
+ end
545
+ end
546
+ end
547
+
548
+ context 'when the validation is declared with allow_blank' do
549
+ context 'given a new record whose attribute is nil' do
550
+ it 'accepts' do
551
+ model = define_model_validating_uniqueness(
552
+ validation_options: { allow_blank: true }
553
+ )
554
+ record = build_record_from(model, attribute_name => nil)
555
+ expect(record).to validate_uniqueness.allow_blank
556
+ end
557
+ end
558
+
559
+ context 'given an existing record whose attribute is nil' do
560
+ it 'accepts' do
561
+ model = define_model_validating_uniqueness(
562
+ validation_options: { allow_blank: true }
563
+ )
564
+ record = create_record_from(model, attribute_name => nil)
565
+ expect(record).to validate_uniqueness.allow_blank
566
+ end
567
+ end
568
+
569
+ context 'given a new record whose attribute is empty' do
570
+ it 'accepts' do
571
+ model = define_model_validating_uniqueness(
572
+ attribute_type: :string,
573
+ validation_options: { allow_blank: true }
574
+ )
575
+ record = build_record_from(model, attribute_name => '')
576
+ expect(record).to validate_uniqueness.allow_blank
577
+ end
578
+ end
579
+
580
+ context 'given an existing record whose attribute is empty' do
581
+ it 'accepts' do
582
+ model = define_model_validating_uniqueness(
583
+ attribute_type: :string,
584
+ validation_options: { allow_blank: true }
585
+ )
586
+ record = create_record_from(model, attribute_name => '')
587
+ expect(record).to validate_uniqueness.allow_blank
588
+ end
589
+ end
590
+
591
+ if active_record_supports_has_secure_password?
592
+ context 'when the model is declared with has_secure_password' do
593
+ context 'given a record whose attribute is nil' do
594
+ it 'accepts' do
595
+ model = define_model_validating_uniqueness(
596
+ validation_options: { allow_blank: true },
597
+ additional_attributes: [{ name: :password_digest, type: :string }]
598
+ ) do |m|
599
+ m.has_secure_password
600
+ end
601
+
602
+ record = build_record_from(model, attribute_name => nil)
603
+
604
+ expect(record).to validate_uniqueness.allow_blank
605
+ end
606
+ end
607
+
608
+ context 'given a record whose attribute is empty' do
609
+ it 'accepts' do
610
+ model = define_model_validating_uniqueness(
611
+ attribute_type: :string,
612
+ validation_options: { allow_blank: true },
613
+ additional_attributes: [{ name: :password_digest, type: :string }]
614
+ ) do |m|
615
+ m.has_secure_password
616
+ end
617
+
618
+ record = build_record_from(model, attribute_name => '')
619
+
620
+ expect(record).to validate_uniqueness.allow_blank
621
+ end
622
+ end
623
+ end
624
+ end
625
+ end
626
+
627
+ context 'when the validation is not declared with allow_blank' do
628
+ context 'given a new record whose attribute is nil' do
629
+ it 'rejects' do
630
+ model = define_model_validating_uniqueness
631
+ record = build_record_from(model, attribute_name => nil)
632
+ expect(record).not_to validate_uniqueness.allow_blank
633
+ end
634
+ end
635
+
636
+ context 'given an existing record whose attribute is nil' do
637
+ it 'rejects' do
638
+ model = define_model_validating_uniqueness
639
+ record = create_record_from(model, attribute_name => nil)
640
+ expect(record).not_to validate_uniqueness.allow_blank
641
+ end
642
+ end
643
+
644
+ context 'given a new record whose attribute is empty' do
645
+ it 'rejects' do
646
+ model = define_model_validating_uniqueness(
647
+ attribute_type: :string
648
+ )
649
+ record = build_record_from(model, attribute_name => '')
650
+ expect(record).not_to validate_uniqueness.allow_blank
651
+ end
652
+ end
653
+
654
+ context 'given an existing record whose attribute is empty' do
655
+ it 'rejects' do
656
+ model = define_model_validating_uniqueness(
657
+ attribute_type: :string
658
+ )
659
+ record = create_record_from(model, attribute_name => '')
660
+ expect(record).not_to validate_uniqueness.allow_blank
661
+ end
662
+ end
663
+ end
664
+
665
+ context 'when testing that a polymorphic *_type column is one of the validation scopes' do
666
+ it 'sets that column to a meaningful value that works with other validations on the same column' do
667
+ user_model = define_model 'User'
668
+ favorite_columns = {
669
+ favoriteable_id: { type: :integer, options: { null: false } },
670
+ favoriteable_type: { type: :string, options: { null: false } }
671
+ }
672
+ favorite_model = define_model 'Favorite', favorite_columns do
673
+ attr_accessible :favoriteable
674
+ belongs_to :favoriteable, polymorphic: true
675
+ validates :favoriteable, presence: true
676
+ validates :favoriteable_id, uniqueness: { scope: :favoriteable_type }
677
+ end
678
+
679
+ user = user_model.create!
680
+ favorite_model.create!(favoriteable: user)
681
+ new_favorite = favorite_model.new
682
+
683
+ expect(new_favorite).
684
+ to validate_uniqueness_of(:favoriteable_id).
685
+ scoped_to(:favoriteable_type)
686
+ end
687
+
688
+ context 'if the model the *_type column refers to is namespaced, and shares the last part of its name with an existing model' do
689
+ it 'still works' do
690
+ define_class 'User'
691
+ define_module 'Models'
692
+ user_model = define_model 'Models::User'
693
+ favorite_columns = {
694
+ favoriteable_id: { type: :integer, options: { null: false } },
695
+ favoriteable_type: { type: :string, options: { null: false } }
696
+ }
697
+ favorite_model = define_model 'Models::Favorite', favorite_columns do
698
+ attr_accessible :favoriteable
699
+ belongs_to :favoriteable, polymorphic: true
700
+ validates :favoriteable, presence: true
701
+ validates :favoriteable_id, uniqueness: { scope: :favoriteable_type }
702
+ end
703
+
704
+ user = user_model.create!
705
+ favorite_model.create!(favoriteable: user)
706
+ new_favorite = favorite_model.new
707
+
708
+ expect(new_favorite).
709
+ to validate_uniqueness_of(:favoriteable_id).
710
+ scoped_to(:favoriteable_type)
711
+ end
712
+ end
713
+ end
714
+
715
+ let(:model_attributes) { {} }
716
+
717
+ def default_attribute
718
+ {
719
+ value_type: :string,
720
+ column_type: :string,
721
+ array: false
722
+ }
723
+ end
724
+
725
+ def normalize_attribute(attribute)
726
+ if attribute.is_a?(Hash)
727
+ if attribute.key?(:type)
728
+ attribute[:value_type] = attribute[:type]
729
+ attribute[:column_type] = attribute[:type]
730
+ end
731
+
732
+ default_attribute.merge(attribute)
733
+ else
734
+ default_attribute.merge(name: attribute)
735
+ end
736
+ end
737
+
738
+ def normalize_attributes(attributes)
739
+ attributes.map do |attribute|
740
+ normalize_attribute(attribute)
741
+ end
742
+ end
743
+
744
+ def column_options_from(attributes)
745
+ attributes.inject({}) do |options, attribute|
746
+ options_for_attribute = options[attribute[:name]] = {
747
+ type: attribute[:column_type],
748
+ options: attribute.fetch(:options, {})
749
+ }
750
+
751
+ if attribute[:array]
752
+ options_for_attribute[:options][:array] = attribute[:array]
753
+ end
754
+
755
+ options
756
+ end
757
+ end
758
+
759
+ def attributes_with_values_for(model)
760
+ model_attributes[model].each_with_object({}) do |attribute, attrs|
761
+ attrs[attribute[:name]] = attribute.fetch(:value) do
762
+ dummy_value_for(
763
+ attribute[:value_type],
764
+ array: attribute[:array]
765
+ )
766
+ end
767
+ end
768
+ end
769
+
770
+ def dummy_value_for(attribute_type, array: false)
771
+ if array
772
+ [ dummy_scalar_value_for(attribute_type) ]
773
+ else
774
+ dummy_scalar_value_for(attribute_type)
775
+ end
776
+ end
777
+
778
+ def dummy_scalar_value_for(attribute_type)
779
+ case attribute_type
780
+ when :string, :text
781
+ 'dummy value'
782
+ when :integer
783
+ 1
784
+ when :date
785
+ Date.today
786
+ when :datetime
787
+ Date.today.to_datetime
788
+ when :time
789
+ Time.now
790
+ when :uuid
791
+ SecureRandom.uuid
792
+ when :boolean
793
+ true
794
+ else
795
+ raise ArgumentError, "Unknown type '#{attribute_type}'"
796
+ end
797
+ end
798
+
799
+ def next_version_of(value, value_type)
800
+ if value.is_a?(Array)
801
+ [ next_version_of(value[0], value_type) ]
802
+ elsif value_type == :uuid
803
+ SecureRandom.uuid
804
+ elsif value.is_a?(Time)
805
+ value + 1
806
+ elsif value.in?([true, false])
807
+ !value
808
+ elsif value.respond_to?(:next)
809
+ value.next
810
+ end
811
+ end
812
+
813
+ def build_record_from(model, extra_attributes = {})
814
+ attributes = attributes_with_values_for(model)
815
+ model.new(attributes.merge(extra_attributes))
816
+ end
817
+
818
+ def create_record_from(model, extra_attributes = {})
819
+ build_record_from(model, extra_attributes).tap do |record|
820
+ record.save!
821
+ end
822
+ end
823
+
824
+ def determine_scope_attribute_names_from(scope_attributes)
825
+ scope_attributes.map do |attribute|
826
+ if attribute.is_a?(Hash)
827
+ attribute[:name]
828
+ else
829
+ attribute
830
+ end
831
+ end
832
+ end
833
+
834
+ def define_model_validating_uniqueness(options = {}, &block)
835
+ attribute_type = options.fetch(:attribute_type, :string)
836
+ attribute_options = options.fetch(:attribute_options, {})
837
+ attribute = {
838
+ name: attribute_name,
839
+ value_type: attribute_type,
840
+ column_type: attribute_type,
841
+ options: attribute_options
842
+ }
843
+ scope_attributes = normalize_attributes(options.fetch(:scopes, []))
844
+ scope_attribute_names = scope_attributes.map { |attr| attr[:name] }
845
+ additional_attributes = normalize_attributes(
846
+ options.fetch(:additional_attributes, [])
847
+ )
848
+ attributes = [attribute] + scope_attributes + additional_attributes
849
+ validation_options = options.fetch(:validation_options, {})
850
+ column_options = column_options_from(attributes)
851
+
852
+ model = define_model(:example, column_options) do |m|
853
+ m.validates_uniqueness_of attribute_name,
854
+ validation_options.merge(scope: scope_attribute_names)
855
+
856
+ attributes.each do |attr|
857
+ m.attr_accessible(attr[:name])
858
+ end
859
+
860
+ block.call(m) if block
861
+ end
862
+
863
+ model_attributes[model] = attributes
864
+
865
+ model
866
+ end
867
+
868
+ def build_record_validating_uniqueness(options = {}, &block)
869
+ model = define_model_validating_uniqueness(options, &block)
870
+ build_record_from(model)
871
+ end
872
+ alias_method :new_record_validating_uniqueness,
873
+ :build_record_validating_uniqueness
874
+
875
+ def create_record_validating_uniqueness(options = {}, &block)
876
+ build_record_validating_uniqueness(options, &block).tap do |record|
877
+ record.save!
878
+ end
879
+ end
880
+ alias_method :existing_record_validating_uniqueness,
881
+ :create_record_validating_uniqueness
882
+
883
+ def build_record_validating_scoped_uniqueness_with_enum(options = {})
884
+ options = options.dup
885
+ enum_scope_attribute =
886
+ normalize_attribute(options.delete(:enum_scope)).
887
+ merge(value_type: :integer, column_type: :integer)
888
+ additional_scopes = options.delete(:additional_scopes) { [] }
889
+ options[:scopes] = [enum_scope_attribute] + additional_scopes
890
+ dummy_enum_values = [:foo, :bar]
891
+
892
+ model = define_model_validating_uniqueness(options)
893
+ model.enum(enum_scope_attribute[:name] => dummy_enum_values)
894
+
895
+ build_record_from(model)
896
+ end
897
+
898
+ def validate_uniqueness
899
+ validate_uniqueness_of(attribute_name)
900
+ end
901
+
902
+ def attribute_name
903
+ :attr
904
+ end
905
+ end