activeentity 0.0.1.beta9 → 0.0.1.beta10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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