activeentity 0.0.1.beta9 → 0.0.1.beta10

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 235dcb06f9c87deacf1304613128ad4deda0f285f3de546c19190b30c481e854
4
- data.tar.gz: 651d6ef3448daf0d44efa992c6be53083a52d721bcc1ad4970b509479eb7e483
3
+ metadata.gz: 3c77d6daeebeba0865b5de31611679a61db4f94f3c5a89d70e33e52f1bcfcf92
4
+ data.tar.gz: 01c96c7a6e7580563fd4ddfa81e8864b2fb1fc0550d853fd92de3eb9baf4eb13
5
5
  SHA512:
6
- metadata.gz: fe5eebc2fc256e1d9af297d3fd4dacdcfea331ce18d0d04f4a1f36d6637943a6dad76442047729e8e243920d0fd30b92a4218a6194333bdd729549ba3e5a3b95
7
- data.tar.gz: 5eab0510837841a772317d375e2fe7367dce1d00dbfa8ef4dd37e09365af7dbe6f3f56406e0d1d0566356e03731f2d5e33861160f699582cdd7a70b5ff2edd87
6
+ metadata.gz: 6fe7aac59d6298f3b952064b78d83f4f566c9dd646e86617af567f3f6c78d0702544e5106f9c54ff8013a07d59f33b6992a703b142ee40e319eaf457ccc0ca30
7
+ data.tar.gz: 5586c053be18d9e9aeef3c91f8362974c98d1819173169f999c2988dca50dbc12c43b44420015594f1e7a4d1639da8e429a037ac47235b9a7c4eefc35f218c47
@@ -10,7 +10,7 @@ module ActiveEntity
10
10
  MAJOR = 0
11
11
  MINOR = 0
12
12
  TINY = 1
13
- PRE = "beta9"
13
+ PRE = "beta10"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -48,3 +48,4 @@ require "active_entity/validations/absence"
48
48
  require "active_entity/validations/length"
49
49
  require "active_entity/validations/subset"
50
50
  require "active_entity/validations/uniqueness_in_embedding"
51
+ require "active_entity/validations/uniqueness_on_active_record"
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Validations
5
+ class UniquenessOnActiveRecordValidator < ActiveModel::EachValidator # :nodoc:
6
+ def initialize(options)
7
+ if options[:conditions] && !options[:conditions].respond_to?(:call)
8
+ raise ArgumentError, "#{options[:conditions]} was passed as :conditions but is not callable. " \
9
+ "Pass a callable instead: `conditions: -> { where(approved: true) }`"
10
+ end
11
+ unless Array(options[:scope]).all? { |scope| scope.respond_to?(:to_sym) }
12
+ raise ArgumentError, "#{options[:scope]} is not supported format for :scope option. " \
13
+ "Pass a symbol or an array of symbols instead: `scope: :user_id`"
14
+ end
15
+
16
+ super
17
+
18
+ @finder_class =
19
+ if options[:ar_class_name].present?
20
+ options[:ar_class_name].safe_constantize
21
+ elsif options[:ar_class].present? && options[:ar_class].is_a?(Class)
22
+ options[:ar_class]
23
+ else
24
+ nil
25
+ end
26
+
27
+ unless @finder_class
28
+ raise ArgumentError, "Must provide one of option :class_name or :class."
29
+ end
30
+ unless @finder_class < ActiveRecord::Base
31
+ raise ArgumentError, "Class must be an Active Record model, but got #{@finder_class}."
32
+ end
33
+ if @finder_class.abstract_class?
34
+ raise ArgumentError, "Class can't be an abstract class."
35
+ end
36
+
37
+ @primary_key_attribute_name = options[:primary_key_attribute_name]
38
+ @present_only = options[:present_only]
39
+ end
40
+
41
+ def validate_each(record, attribute, value)
42
+ value = map_enum_attribute(@finder_class, attribute, value)
43
+ if @present_only && value.blank?
44
+ return
45
+ end
46
+
47
+ relation = build_relation(@finder_class, attribute, value)
48
+ if @primary_key_attribute_name.present?
49
+ primary_key_attribute = record.read_attribute(@primary_key_attribute_name)
50
+ if primary_key_attribute.present?
51
+ if @finder_class.primary_key
52
+ relation = relation.where.not(@finder_class.primary_key => primary_key_attribute)
53
+ else
54
+ raise ActiveRecord::UnknownPrimaryKey.new(@finder_class, "Can not validate uniqueness for persisted record without primary key.")
55
+ end
56
+ end
57
+ end
58
+ relation = scope_relation(record, relation)
59
+ relation = relation.merge(options[:conditions]) if options[:conditions]
60
+
61
+ if relation.exists?
62
+ error_options = options.except(:case_sensitive, :scope, :conditions, :ar_class, :ar_class_name, :primary_key_attribute_name)
63
+ error_options[:value] = value
64
+
65
+ record.errors.add(attribute, :taken, error_options)
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def build_relation(klass, attribute, value)
72
+ relation = klass.unscoped
73
+ comparison = relation.bind_attribute(attribute, value) do |attr, bind|
74
+ return relation.none! if bind.unboundable?
75
+
76
+ if !options.key?(:case_sensitive) || bind.nil?
77
+ klass.connection.default_uniqueness_comparison(attr, bind, klass)
78
+ elsif options[:case_sensitive]
79
+ klass.connection.case_sensitive_comparison(attr, bind)
80
+ else
81
+ # will use SQL LOWER function before comparison, unless it detects a case insensitive collation
82
+ klass.connection.case_insensitive_comparison(attr, bind)
83
+ end
84
+ end
85
+
86
+ relation.where!(comparison)
87
+ end
88
+
89
+ def scope_relation(record, relation)
90
+ Array(options[:scope]).each do |scope_item|
91
+ scope_value = if record.class._reflect_on_association(scope_item)
92
+ record.association(scope_item).reader
93
+ else
94
+ record._read_attribute(scope_item)
95
+ end
96
+ relation = relation.where(scope_item => scope_value)
97
+ end
98
+
99
+ relation
100
+ end
101
+
102
+ def map_enum_attribute(klass, attribute, value)
103
+ mapping = klass.defined_enums[attribute.to_s]
104
+ value = mapping[value] if value && mapping
105
+ value
106
+ end
107
+ end
108
+
109
+ module ClassMethods
110
+ # Validates whether the value of the specified attributes are unique
111
+ # across the system. Useful for making sure that only one user
112
+ # can be named "davidhh".
113
+ #
114
+ # class Person < ActiveRecord::Base
115
+ # validates_uniqueness_of :user_name
116
+ # end
117
+ #
118
+ # It can also validate whether the value of the specified attributes are
119
+ # unique based on a <tt>:scope</tt> parameter:
120
+ #
121
+ # class Person < ActiveRecord::Base
122
+ # validates_uniqueness_of :user_name, scope: :account_id
123
+ # end
124
+ #
125
+ # Or even multiple scope parameters. For example, making sure that a
126
+ # teacher can only be on the schedule once per semester for a particular
127
+ # class.
128
+ #
129
+ # class TeacherSchedule < ActiveRecord::Base
130
+ # validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id]
131
+ # end
132
+ #
133
+ # It is also possible to limit the uniqueness constraint to a set of
134
+ # records matching certain conditions. In this example archived articles
135
+ # are not being taken into consideration when validating uniqueness
136
+ # of the title attribute:
137
+ #
138
+ # class Article < ActiveRecord::Base
139
+ # validates_uniqueness_of :title, conditions: -> { where.not(status: 'archived') }
140
+ # end
141
+ #
142
+ # When the record is created, a check is performed to make sure that no
143
+ # record exists in the database with the given value for the specified
144
+ # attribute (that maps to a column). When the record is updated,
145
+ # the same check is made but disregarding the record itself.
146
+ #
147
+ # Configuration options:
148
+ #
149
+ # * <tt>:message</tt> - Specifies a custom error message (default is:
150
+ # "has already been taken").
151
+ # * <tt>:scope</tt> - One or more columns by which to limit the scope of
152
+ # the uniqueness constraint.
153
+ # * <tt>:conditions</tt> - Specify the conditions to be included as a
154
+ # <tt>WHERE</tt> SQL fragment to limit the uniqueness constraint lookup
155
+ # (e.g. <tt>conditions: -> { where(status: 'active') }</tt>).
156
+ # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by
157
+ # non-text columns (+true+ by default).
158
+ # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the
159
+ # attribute is +nil+ (default is +false+).
160
+ # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the
161
+ # attribute is blank (default is +false+).
162
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
163
+ # if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
164
+ # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
165
+ # proc or string should return or evaluate to a +true+ or +false+ value.
166
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to
167
+ # determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
168
+ # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
169
+ # method, proc or string should return or evaluate to a +true+ or +false+
170
+ # value.
171
+ #
172
+ # === Concurrency and integrity
173
+ #
174
+ # Using this validation method in conjunction with
175
+ # {ActiveRecord::Base#save}[rdoc-ref:Persistence#save]
176
+ # does not guarantee the absence of duplicate record insertions, because
177
+ # uniqueness checks on the application level are inherently prone to race
178
+ # conditions. For example, suppose that two users try to post a Comment at
179
+ # the same time, and a Comment's title must be unique. At the database-level,
180
+ # the actions performed by these users could be interleaved in the following manner:
181
+ #
182
+ # User 1 | User 2
183
+ # ------------------------------------+--------------------------------------
184
+ # # User 1 checks whether there's |
185
+ # # already a comment with the title |
186
+ # # 'My Post'. This is not the case. |
187
+ # SELECT * FROM comments |
188
+ # WHERE title = 'My Post' |
189
+ # |
190
+ # | # User 2 does the same thing and also
191
+ # | # infers that their title is unique.
192
+ # | SELECT * FROM comments
193
+ # | WHERE title = 'My Post'
194
+ # |
195
+ # # User 1 inserts their comment. |
196
+ # INSERT INTO comments |
197
+ # (title, content) VALUES |
198
+ # ('My Post', 'hi!') |
199
+ # |
200
+ # | # User 2 does the same thing.
201
+ # | INSERT INTO comments
202
+ # | (title, content) VALUES
203
+ # | ('My Post', 'hello!')
204
+ # |
205
+ # | # ^^^^^^
206
+ # | # Boom! We now have a duplicate
207
+ # | # title!
208
+ #
209
+ # The best way to work around this problem is to add a unique index to the database table using
210
+ # {connection.add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index].
211
+ # In the rare case that a race condition occurs, the database will guarantee
212
+ # the field's uniqueness.
213
+ #
214
+ # When the database catches such a duplicate insertion,
215
+ # {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will raise an ActiveRecord::StatementInvalid
216
+ # exception. You can either choose to let this error propagate (which
217
+ # will result in the default Rails exception page being shown), or you
218
+ # can catch it and restart the transaction (e.g. by telling the user
219
+ # that the title already exists, and asking them to re-enter the title).
220
+ # This technique is also known as
221
+ # {optimistic concurrency control}[https://en.wikipedia.org/wiki/Optimistic_concurrency_control].
222
+ #
223
+ # The bundled ActiveRecord::ConnectionAdapters distinguish unique index
224
+ # constraint errors from other types of database errors by throwing an
225
+ # ActiveRecord::RecordNotUnique exception. For other adapters you will
226
+ # have to parse the (database-specific) exception message to detect such
227
+ # a case.
228
+ #
229
+ # The following bundled adapters throw the ActiveRecord::RecordNotUnique exception:
230
+ #
231
+ # * ActiveRecord::ConnectionAdapters::Mysql2Adapter.
232
+ # * ActiveRecord::ConnectionAdapters::SQLite3Adapter.
233
+ # * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.
234
+ def validates_uniqueness_on_active_record_of(*attr_names)
235
+ validates_with UniquenessOnActiveRecordValidator, _merge_attributes(attr_names)
236
+ end
237
+ end
238
+ end
239
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activeentity
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1.beta9
4
+ version: 0.0.1.beta10
5
5
  platform: ruby
6
6
  authors:
7
7
  - jasl
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-05-20 00:00:00.000000000 Z
11
+ date: 2019-05-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -130,6 +130,7 @@ files:
130
130
  - lib/active_entity/validations/presence.rb
131
131
  - lib/active_entity/validations/subset.rb
132
132
  - lib/active_entity/validations/uniqueness_in_embedding.rb
133
+ - lib/active_entity/validations/uniqueness_on_active_record.rb
133
134
  - lib/active_entity/version.rb
134
135
  - lib/core_ext/array_without_blank.rb
135
136
  - lib/tasks/active_entity_tasks.rake