shoulda-matchers 2.8.0 → 3.0.0.rc1

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.
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