activeentity 0.0.1.beta14 → 0.0.1.beta15
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 +4 -4
- data/MIT-LICENSE +1 -1
- data/README.md +4 -4
- data/Rakefile +7 -7
- data/lib/active_entity.rb +29 -7
- data/lib/active_entity/aggregations.rb +2 -1
- data/lib/active_entity/associations.rb +46 -24
- data/lib/active_entity/associations/{embedded → embeds}/association.rb +2 -2
- data/lib/active_entity/associations/{embedded → embeds}/builder/association.rb +1 -1
- data/lib/active_entity/associations/{embedded → embeds}/builder/collection_association.rb +1 -1
- data/lib/active_entity/associations/{embedded → embeds}/builder/embedded_in.rb +1 -1
- data/lib/active_entity/associations/{embedded → embeds}/builder/embeds_many.rb +1 -1
- data/lib/active_entity/associations/{embedded → embeds}/builder/embeds_one.rb +1 -1
- data/lib/active_entity/associations/{embedded → embeds}/builder/singular_association.rb +1 -1
- data/lib/active_entity/associations/{embedded → embeds}/collection_association.rb +1 -1
- data/lib/active_entity/associations/{embedded → embeds}/collection_proxy.rb +2 -2
- data/lib/active_entity/associations/{embedded → embeds}/embedded_in_association.rb +1 -1
- data/lib/active_entity/associations/{embedded → embeds}/embeds_many_association.rb +1 -1
- data/lib/active_entity/associations/{embedded → embeds}/embeds_one_association.rb +2 -1
- data/lib/active_entity/associations/{embedded → embeds}/singular_association.rb +1 -1
- data/lib/active_entity/attribute_assignment.rb +9 -3
- data/lib/active_entity/attribute_methods.rb +12 -11
- data/lib/active_entity/attribute_methods/before_type_cast.rb +1 -1
- data/lib/active_entity/attribute_methods/dirty.rb +13 -0
- data/lib/active_entity/attribute_methods/primary_key.rb +1 -1
- data/lib/active_entity/attribute_methods/query.rb +11 -4
- data/lib/active_entity/attribute_methods/read.rb +1 -3
- data/lib/active_entity/attribute_methods/time_zone_conversion.rb +2 -0
- data/lib/active_entity/attribute_methods/write.rb +4 -6
- data/lib/active_entity/attributes.rb +76 -2
- data/lib/active_entity/base.rb +3 -12
- data/lib/active_entity/core.rb +97 -39
- data/lib/active_entity/define_callbacks.rb +4 -0
- data/lib/active_entity/enum.rb +30 -4
- data/lib/active_entity/errors.rb +0 -11
- data/lib/active_entity/gem_version.rb +1 -1
- data/lib/active_entity/inheritance.rb +4 -106
- data/lib/active_entity/integration.rb +1 -1
- data/lib/active_entity/model_schema.rb +0 -12
- data/lib/active_entity/nested_attributes.rb +5 -12
- data/lib/active_entity/railtie.rb +61 -1
- data/lib/active_entity/readonly_attributes.rb +9 -1
- data/lib/active_entity/reflection.rb +22 -19
- data/lib/active_entity/serialization.rb +9 -6
- data/lib/active_entity/store.rb +51 -2
- data/lib/active_entity/type.rb +8 -8
- data/lib/active_entity/type/registry.rb +5 -5
- data/lib/active_entity/{validate_embedded_association.rb → validate_embeds_association.rb} +6 -6
- data/lib/active_entity/validations.rb +2 -6
- data/lib/active_entity/validations/associated.rb +1 -1
- data/lib/active_entity/validations/{uniqueness_in_embedding.rb → uniqueness_in_embeds.rb} +1 -1
- data/lib/active_entity/validations/uniqueness_on_active_record.rb +46 -40
- metadata +27 -30
- data/lib/active_entity/type/decimal_without_scale.rb +0 -15
- data/lib/active_entity/type/hash_lookup_type_map.rb +0 -25
- data/lib/active_entity/type/type_map.rb +0 -62
- data/lib/tasks/active_entity_tasks.rake +0 -6
@@ -17,7 +17,7 @@ module ActiveEntity
|
|
17
17
|
def create(macro, name, scope, options, ar_or_ae)
|
18
18
|
reflection_class_for(macro).new(name, scope, options, ar_or_ae)
|
19
19
|
|
20
|
-
# TODO: Support bridge to Active
|
20
|
+
# TODO: Support bridge to Active Entity
|
21
21
|
# reflection = reflection_class_for(macro).new(name, scope, options, ar_or_ae)
|
22
22
|
# options[:through] ? ActiveRecord::ThroughReflection.new(reflection) : reflection
|
23
23
|
end
|
@@ -33,6 +33,7 @@ module ActiveEntity
|
|
33
33
|
end
|
34
34
|
|
35
35
|
private
|
36
|
+
|
36
37
|
def reflection_class_for(macro)
|
37
38
|
case macro
|
38
39
|
when :composed_of
|
@@ -77,21 +78,21 @@ module ActiveEntity
|
|
77
78
|
#
|
78
79
|
def reflections
|
79
80
|
@__reflections ||= begin
|
80
|
-
|
81
|
+
ref = {}
|
81
82
|
|
82
|
-
|
83
|
-
|
83
|
+
_reflections.each do |name, reflection|
|
84
|
+
parent_reflection = reflection.parent_reflection
|
84
85
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
86
|
+
if parent_reflection
|
87
|
+
parent_name = parent_reflection.name
|
88
|
+
ref[parent_name.to_s] = parent_reflection
|
89
|
+
else
|
90
|
+
ref[name] = reflection
|
91
|
+
end
|
92
|
+
end
|
92
93
|
|
93
|
-
|
94
|
-
|
94
|
+
ref
|
95
|
+
end
|
95
96
|
end
|
96
97
|
|
97
98
|
# Returns an array of AssociationReflection objects for all the
|
@@ -184,6 +185,7 @@ module ActiveEntity
|
|
184
185
|
end
|
185
186
|
|
186
187
|
protected
|
188
|
+
|
187
189
|
def actual_source_reflection # FIXME: this is a horrible name
|
188
190
|
self
|
189
191
|
end
|
@@ -242,12 +244,13 @@ module ActiveEntity
|
|
242
244
|
def ==(other_aggregation)
|
243
245
|
super ||
|
244
246
|
other_aggregation.kind_of?(self.class) &&
|
245
|
-
|
246
|
-
|
247
|
-
|
247
|
+
name == other_aggregation.name &&
|
248
|
+
!other_aggregation.options.nil? &&
|
249
|
+
active_entity == other_aggregation.active_entity
|
248
250
|
end
|
249
251
|
|
250
252
|
private
|
253
|
+
|
251
254
|
def derive_class_name
|
252
255
|
name.to_s.camelize
|
253
256
|
end
|
@@ -417,7 +420,7 @@ module ActiveEntity
|
|
417
420
|
def collection?; true; end
|
418
421
|
|
419
422
|
def association_class
|
420
|
-
Associations::
|
423
|
+
Associations::Embeds::EmbedsManyAssociation
|
421
424
|
end
|
422
425
|
end
|
423
426
|
|
@@ -427,7 +430,7 @@ module ActiveEntity
|
|
427
430
|
def embeds_one?; true; end
|
428
431
|
|
429
432
|
def association_class
|
430
|
-
Associations::
|
433
|
+
Associations::Embeds::EmbedsOneAssociation
|
431
434
|
end
|
432
435
|
end
|
433
436
|
|
@@ -437,7 +440,7 @@ module ActiveEntity
|
|
437
440
|
def embedded_in?; true; end
|
438
441
|
|
439
442
|
def association_class
|
440
|
-
Associations::
|
443
|
+
Associations::Embeds::EmbeddedInAssociation
|
441
444
|
end
|
442
445
|
end
|
443
446
|
end
|
@@ -10,14 +10,17 @@ module ActiveEntity #:nodoc:
|
|
10
10
|
self.include_root_in_json = false
|
11
11
|
end
|
12
12
|
|
13
|
-
def serializable_hash(
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
def serializable_hash(options = nil)
|
14
|
+
options = options ? options.dup : {}
|
15
|
+
|
16
|
+
include_embeds = options.delete :include_embeds
|
17
|
+
if include_embeds
|
18
|
+
includes = Array.wrap(options[:include]).concat(self.class.embeds_association_names)
|
19
|
+
options[:include] ||= []
|
20
|
+
options[:include].concat includes
|
17
21
|
end
|
18
22
|
|
19
|
-
|
20
|
-
# options[:except] |= Array(self.class.inheritance_attribute)
|
23
|
+
options[:except] = Array(options[:except]).map(&:to_s)
|
21
24
|
|
22
25
|
super(options)
|
23
26
|
end
|
data/lib/active_entity/store.rb
CHANGED
@@ -11,13 +11,19 @@ module ActiveEntity
|
|
11
11
|
# of the model. This is very helpful for easily exposing store keys to a form or elsewhere that's
|
12
12
|
# already built around just accessing attributes on the model.
|
13
13
|
#
|
14
|
+
# Every accessor comes with dirty tracking methods (+key_changed?+, +key_was+ and +key_change+) and
|
15
|
+
# methods to access the changes made during the last save (+saved_change_to_key?+, +saved_change_to_key+ and
|
16
|
+
# +key_before_last_save+).
|
17
|
+
#
|
18
|
+
# NOTE: There is no +key_will_change!+ method for accessors, use +store_will_change!+ instead.
|
19
|
+
#
|
14
20
|
# Make sure that you declare the database column used for the serialized store as a text, so there's
|
15
21
|
# plenty of room.
|
16
22
|
#
|
17
23
|
# You can set custom coder to encode/decode your serialized attributes to/from different formats.
|
18
24
|
# JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
|
19
25
|
#
|
20
|
-
# NOTE: If you are using structured database data types (
|
26
|
+
# NOTE: If you are using structured database data types (e.g. PostgreSQL +hstore+/+json+, or MySQL 5.7+
|
21
27
|
# +json+) there is no need for the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store].
|
22
28
|
# Simply use {.store_accessor}[rdoc-ref:ClassMethods#store_accessor] instead to generate
|
23
29
|
# the accessor methods. Be aware that these columns use a string keyed hash and do not allow access
|
@@ -49,6 +55,12 @@ module ActiveEntity
|
|
49
55
|
# u.settings[:country] # => 'Denmark'
|
50
56
|
# u.settings['country'] # => 'Denmark'
|
51
57
|
#
|
58
|
+
# # Dirty tracking
|
59
|
+
# u.color = 'green'
|
60
|
+
# u.color_changed? # => true
|
61
|
+
# u.color_was # => 'black'
|
62
|
+
# u.color_change # => ['black', 'red']
|
63
|
+
#
|
52
64
|
# # Add additional accessors to an existing store through store_accessor
|
53
65
|
# class SuperUser < User
|
54
66
|
# store_accessor :settings, :privileges, :servants
|
@@ -91,7 +103,7 @@ module ActiveEntity
|
|
91
103
|
module ClassMethods
|
92
104
|
def store(store_attribute, options = {})
|
93
105
|
serialize store_attribute, IndifferentCoder.new(store_attribute, options[:coder])
|
94
|
-
store_accessor(store_attribute, options[:accessors], options.slice(:prefix, :suffix)) if options.has_key? :accessors
|
106
|
+
store_accessor(store_attribute, options[:accessors], **options.slice(:prefix, :suffix)) if options.has_key? :accessors
|
95
107
|
end
|
96
108
|
|
97
109
|
def store_accessor(store_attribute, *keys, prefix: nil, suffix: nil)
|
@@ -127,6 +139,42 @@ module ActiveEntity
|
|
127
139
|
define_method(accessor_key) do
|
128
140
|
read_store_attribute(store_attribute, key)
|
129
141
|
end
|
142
|
+
|
143
|
+
define_method("#{accessor_key}_changed?") do
|
144
|
+
return false unless attribute_changed?(store_attribute)
|
145
|
+
prev_store, new_store = changes[store_attribute]
|
146
|
+
prev_store&.dig(key) != new_store&.dig(key)
|
147
|
+
end
|
148
|
+
|
149
|
+
define_method("#{accessor_key}_change") do
|
150
|
+
return unless attribute_changed?(store_attribute)
|
151
|
+
prev_store, new_store = changes[store_attribute]
|
152
|
+
[prev_store&.dig(key), new_store&.dig(key)]
|
153
|
+
end
|
154
|
+
|
155
|
+
define_method("#{accessor_key}_was") do
|
156
|
+
return unless attribute_changed?(store_attribute)
|
157
|
+
prev_store, _new_store = changes[store_attribute]
|
158
|
+
prev_store&.dig(key)
|
159
|
+
end
|
160
|
+
|
161
|
+
define_method("saved_change_to_#{accessor_key}?") do
|
162
|
+
return false unless saved_change_to_attribute?(store_attribute)
|
163
|
+
prev_store, new_store = saved_change_to_attribute(store_attribute)
|
164
|
+
prev_store&.dig(key) != new_store&.dig(key)
|
165
|
+
end
|
166
|
+
|
167
|
+
define_method("saved_change_to_#{accessor_key}") do
|
168
|
+
return unless saved_change_to_attribute?(store_attribute)
|
169
|
+
prev_store, new_store = saved_change_to_attribute(store_attribute)
|
170
|
+
[prev_store&.dig(key), new_store&.dig(key)]
|
171
|
+
end
|
172
|
+
|
173
|
+
define_method("#{accessor_key}_before_last_save") do
|
174
|
+
return unless saved_change_to_attribute?(store_attribute)
|
175
|
+
prev_store, _new_store = saved_change_to_attribute(store_attribute)
|
176
|
+
prev_store&.dig(key)
|
177
|
+
end
|
130
178
|
end
|
131
179
|
end
|
132
180
|
|
@@ -155,6 +203,7 @@ module ActiveEntity
|
|
155
203
|
end
|
156
204
|
|
157
205
|
private
|
206
|
+
|
158
207
|
def read_store_attribute(store_attribute, key) # :doc:
|
159
208
|
accessor = store_accessor_for(store_attribute)
|
160
209
|
accessor.read(self, store_attribute, key)
|
data/lib/active_entity/type.rb
CHANGED
@@ -6,7 +6,6 @@ require "active_entity/type/internal/timezone"
|
|
6
6
|
|
7
7
|
require "active_entity/type/date"
|
8
8
|
require "active_entity/type/date_time"
|
9
|
-
require "active_entity/type/decimal_without_scale"
|
10
9
|
require "active_entity/type/json"
|
11
10
|
require "active_entity/type/time"
|
12
11
|
require "active_entity/type/text"
|
@@ -18,12 +17,9 @@ require "active_entity/type/modifiers/array_without_blank"
|
|
18
17
|
require "active_entity/type/serialized"
|
19
18
|
require "active_entity/type/registry"
|
20
19
|
|
21
|
-
require "active_entity/type/type_map"
|
22
|
-
require "active_entity/type/hash_lookup_type_map"
|
23
|
-
|
24
20
|
module ActiveEntity
|
25
21
|
module Type
|
26
|
-
@registry =
|
22
|
+
@registry = Registry.new
|
27
23
|
|
28
24
|
class << self
|
29
25
|
attr_accessor :registry # :nodoc:
|
@@ -31,8 +27,12 @@ module ActiveEntity
|
|
31
27
|
|
32
28
|
# Add a new type to the registry, allowing it to be referenced as a
|
33
29
|
# symbol by {ActiveEntity::Base.attribute}[rdoc-ref:Attributes::ClassMethods#attribute].
|
34
|
-
#
|
35
|
-
# <tt>
|
30
|
+
# If your type is only meant to be used with a specific database adapter, you can
|
31
|
+
# do so by passing <tt>adapter: :postgresql</tt>. If your type has the same
|
32
|
+
# name as a native type for the current adapter, an exception will be
|
33
|
+
# raised unless you specify an +:override+ option. <tt>override: true</tt> will
|
34
|
+
# cause your type to be used instead of the native type. <tt>override:
|
35
|
+
# false</tt> will cause the native type to be used over yours if one exists.
|
36
36
|
def register(type_name, klass = nil, **options, &block)
|
37
37
|
registry.register(type_name, klass, **options, &block)
|
38
38
|
end
|
@@ -46,7 +46,6 @@ module ActiveEntity
|
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
|
-
Helpers = ActiveModel::Type::Helpers
|
50
49
|
BigInteger = ActiveModel::Type::BigInteger
|
51
50
|
Binary = ActiveModel::Type::Binary
|
52
51
|
Boolean = ActiveModel::Type::Boolean
|
@@ -67,6 +66,7 @@ module ActiveEntity
|
|
67
66
|
register(:decimal, Type::Decimal, override: false)
|
68
67
|
register(:float, Type::Float, override: false)
|
69
68
|
register(:integer, Type::Integer, override: false)
|
69
|
+
register(:unsigned_integer, Type::UnsignedInteger, override: false)
|
70
70
|
register(:json, Type::Json, override: false)
|
71
71
|
register(:string, Type::String, override: false)
|
72
72
|
register(:text, Type::Text, override: false)
|
@@ -6,8 +6,8 @@ module ActiveEntity
|
|
6
6
|
# :stopdoc:
|
7
7
|
module Type
|
8
8
|
class Registry < ActiveModel::Type::Registry
|
9
|
-
def add_modifier(options, klass)
|
10
|
-
registrations << DecorationRegistration.new(options, klass)
|
9
|
+
def add_modifier(options, klass, **args)
|
10
|
+
registrations << DecorationRegistration.new(options, klass, **args)
|
11
11
|
end
|
12
12
|
|
13
13
|
private
|
@@ -16,9 +16,9 @@ module ActiveEntity
|
|
16
16
|
Registration
|
17
17
|
end
|
18
18
|
|
19
|
-
def find_registration(symbol, *args)
|
19
|
+
def find_registration(symbol, *args, **kwargs)
|
20
20
|
registrations
|
21
|
-
.select { |registration| registration.matches?(symbol, *args) }
|
21
|
+
.select { |registration| registration.matches?(symbol, *args, **kwargs) }
|
22
22
|
.max
|
23
23
|
end
|
24
24
|
end
|
@@ -56,7 +56,7 @@ module ActiveEntity
|
|
56
56
|
end
|
57
57
|
|
58
58
|
class DecorationRegistration < Registration
|
59
|
-
def initialize(options, klass)
|
59
|
+
def initialize(options, klass, **)
|
60
60
|
@options = options
|
61
61
|
@klass = klass
|
62
62
|
end
|
@@ -127,12 +127,12 @@ module ActiveEntity
|
|
127
127
|
# Now it _is_ removed from the database:
|
128
128
|
#
|
129
129
|
# Comment.find_by(id: id).nil? # => true
|
130
|
-
module
|
130
|
+
module ValidateEmbedsAssociation
|
131
131
|
extend ActiveSupport::Concern
|
132
132
|
|
133
133
|
module AssociationBuilderExtension #:nodoc:
|
134
134
|
def self.build(model, reflection)
|
135
|
-
model.send(:
|
135
|
+
model.send(:add_embeds_associations_validation_callbacks, reflection)
|
136
136
|
end
|
137
137
|
|
138
138
|
def self.valid_options
|
@@ -141,7 +141,7 @@ module ActiveEntity
|
|
141
141
|
end
|
142
142
|
|
143
143
|
included do
|
144
|
-
Associations::
|
144
|
+
Associations::Embeds::Builder::Association.extensions << AssociationBuilderExtension
|
145
145
|
|
146
146
|
unless respond_to?(:index_nested_attribute_errors)
|
147
147
|
mattr_accessor :index_nested_attribute_errors, instance_writer: false, default: false
|
@@ -180,11 +180,11 @@ module ActiveEntity
|
|
180
180
|
# the callbacks to get defined multiple times, there are guards that
|
181
181
|
# check if the save or validation methods have already been defined
|
182
182
|
# before actually defining them.
|
183
|
-
def
|
184
|
-
|
183
|
+
def add_embeds_associations_validation_callbacks(reflection)
|
184
|
+
define_embeds_associations_validation_callbacks(reflection)
|
185
185
|
end
|
186
186
|
|
187
|
-
def
|
187
|
+
def define_embeds_associations_validation_callbacks(reflection)
|
188
188
|
validation_method = :"validate_associated_records_for_#{reflection.name}"
|
189
189
|
if reflection.validate? && !method_defined?(validation_method)
|
190
190
|
if reflection.collection?
|
@@ -23,7 +23,7 @@ module ActiveEntity
|
|
23
23
|
# \Validations with no <tt>:on</tt> option will run no matter the context. \Validations with
|
24
24
|
# some <tt>:on</tt> option will only run in the specified context.
|
25
25
|
def valid?(context = nil)
|
26
|
-
context ||=
|
26
|
+
context ||= :default
|
27
27
|
output = super(context)
|
28
28
|
errors.empty? && output
|
29
29
|
end
|
@@ -32,10 +32,6 @@ module ActiveEntity
|
|
32
32
|
|
33
33
|
private
|
34
34
|
|
35
|
-
def default_validation_context
|
36
|
-
:default
|
37
|
-
end
|
38
|
-
|
39
35
|
def perform_validations(options = {})
|
40
36
|
options[:validate] == false || valid?(options[:context])
|
41
37
|
end
|
@@ -47,5 +43,5 @@ require "active_entity/validations/presence"
|
|
47
43
|
require "active_entity/validations/absence"
|
48
44
|
require "active_entity/validations/length"
|
49
45
|
require "active_entity/validations/subset"
|
50
|
-
require "active_entity/validations/
|
46
|
+
require "active_entity/validations/uniqueness_in_embeds"
|
51
47
|
require "active_entity/validations/uniqueness_on_active_record"
|
@@ -5,7 +5,7 @@ module ActiveEntity
|
|
5
5
|
class AssociatedValidator < ActiveModel::EachValidator #:nodoc:
|
6
6
|
def validate_each(record, attribute, value)
|
7
7
|
if Array(value).reject { |r| valid_object?(r) }.any?
|
8
|
-
record.errors.add(attribute, :invalid, options.merge(value: value))
|
8
|
+
record.errors.add(attribute, :invalid, **options.merge(value: value))
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module ActiveEntity
|
4
4
|
module Validations
|
5
|
-
class
|
5
|
+
class UniquenessInEmbedsValidator < ActiveModel::EachValidator # :nodoc:
|
6
6
|
ERROR_MESSAGE = "`key` option of the configuration hash must be symbol or array of symbols."
|
7
7
|
|
8
8
|
def check_validity!
|
@@ -12,62 +12,68 @@ module ActiveEntity
|
|
12
12
|
raise ArgumentError, "#{options[:scope]} is not supported format for :scope option. " \
|
13
13
|
"Pass a symbol or an array of symbols instead: `scope: :user_id`"
|
14
14
|
end
|
15
|
-
|
16
15
|
super
|
17
16
|
|
18
|
-
@
|
19
|
-
if options[:
|
20
|
-
options[:
|
21
|
-
elsif options[:
|
22
|
-
options[:
|
17
|
+
@klass =
|
18
|
+
if options[:class].present?
|
19
|
+
options[:class]
|
20
|
+
elsif options[:class_name].present?
|
21
|
+
options[:class_name].safe_constantize
|
23
22
|
else
|
24
23
|
nil
|
25
24
|
end
|
26
25
|
|
27
|
-
unless @
|
26
|
+
unless @klass
|
28
27
|
raise ArgumentError, "Must provide one of option :class_name or :class."
|
29
28
|
end
|
30
|
-
unless @
|
31
|
-
raise ArgumentError, "Class must be an Active
|
29
|
+
unless @klass < ActiveRecord::Base
|
30
|
+
raise ArgumentError, "Class must be an Active Entity model, but got #{@finder_class}."
|
32
31
|
end
|
33
|
-
if @
|
32
|
+
if @klass.abstract_class?
|
34
33
|
raise ArgumentError, "Class can't be an abstract class."
|
35
34
|
end
|
36
|
-
|
37
|
-
@primary_key_attribute_name = options[:primary_key_attribute_name]
|
38
|
-
@present_only = options[:present_only]
|
39
35
|
end
|
40
36
|
|
41
37
|
def validate_each(record, attribute, value)
|
42
|
-
|
43
|
-
|
44
|
-
return
|
45
|
-
end
|
38
|
+
finder_class = find_finder_class_for(record)
|
39
|
+
value = map_enum_attribute(finder_class, attribute, value)
|
46
40
|
|
47
|
-
relation = build_relation(
|
48
|
-
if
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
else
|
54
|
-
raise ActiveRecord::UnknownPrimaryKey.new(@finder_class, "Can not validate uniqueness for persisted record without primary key.")
|
55
|
-
end
|
41
|
+
relation = build_relation(finder_class, attribute, value)
|
42
|
+
if record.persisted?
|
43
|
+
if finder_class.primary_key
|
44
|
+
relation = relation.where.not(finder_class.primary_key => record.id_in_database)
|
45
|
+
else
|
46
|
+
raise UnknownPrimaryKey.new(finder_class, "Cannot validate uniqueness for persisted record without primary key.")
|
56
47
|
end
|
57
48
|
end
|
58
49
|
relation = scope_relation(record, relation)
|
59
50
|
relation = relation.merge(options[:conditions]) if options[:conditions]
|
60
51
|
|
61
52
|
if relation.exists?
|
62
|
-
error_options = options.except(:case_sensitive, :scope, :conditions
|
53
|
+
error_options = options.except(:case_sensitive, :scope, :conditions)
|
63
54
|
error_options[:value] = value
|
64
55
|
|
65
|
-
record.errors.add(attribute, :taken, error_options)
|
56
|
+
record.errors.add(attribute, :taken, **error_options)
|
66
57
|
end
|
67
58
|
end
|
68
59
|
|
69
60
|
private
|
70
61
|
|
62
|
+
# The check for an existing value should be run from a class that
|
63
|
+
# isn't abstract. This means working down from the current class
|
64
|
+
# (self), to the first non-abstract class. Since classes don't know
|
65
|
+
# their subclasses, we have to build the hierarchy between self and
|
66
|
+
# the record's class.
|
67
|
+
def find_finder_class_for(record)
|
68
|
+
class_hierarchy = [record.class]
|
69
|
+
|
70
|
+
while class_hierarchy.first != @klass
|
71
|
+
class_hierarchy.unshift(class_hierarchy.first.superclass)
|
72
|
+
end
|
73
|
+
|
74
|
+
class_hierarchy.detect { |klass| !klass.abstract_class? }
|
75
|
+
end
|
76
|
+
|
71
77
|
def build_relation(klass, attribute, value)
|
72
78
|
relation = klass.unscoped
|
73
79
|
comparison = relation.bind_attribute(attribute, value) do |attr, bind|
|
@@ -111,14 +117,14 @@ module ActiveEntity
|
|
111
117
|
# across the system. Useful for making sure that only one user
|
112
118
|
# can be named "davidhh".
|
113
119
|
#
|
114
|
-
# class Person <
|
120
|
+
# class Person < ActiveEntity::Base
|
115
121
|
# validates_uniqueness_of :user_name
|
116
122
|
# end
|
117
123
|
#
|
118
124
|
# It can also validate whether the value of the specified attributes are
|
119
125
|
# unique based on a <tt>:scope</tt> parameter:
|
120
126
|
#
|
121
|
-
# class Person <
|
127
|
+
# class Person < ActiveEntity::Base
|
122
128
|
# validates_uniqueness_of :user_name, scope: :account_id
|
123
129
|
# end
|
124
130
|
#
|
@@ -126,7 +132,7 @@ module ActiveEntity
|
|
126
132
|
# teacher can only be on the schedule once per semester for a particular
|
127
133
|
# class.
|
128
134
|
#
|
129
|
-
# class TeacherSchedule <
|
135
|
+
# class TeacherSchedule < ActiveEntity::Base
|
130
136
|
# validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id]
|
131
137
|
# end
|
132
138
|
#
|
@@ -135,7 +141,7 @@ module ActiveEntity
|
|
135
141
|
# are not being taken into consideration when validating uniqueness
|
136
142
|
# of the title attribute:
|
137
143
|
#
|
138
|
-
# class Article <
|
144
|
+
# class Article < ActiveEntity::Base
|
139
145
|
# validates_uniqueness_of :title, conditions: -> { where.not(status: 'archived') }
|
140
146
|
# end
|
141
147
|
#
|
@@ -172,7 +178,7 @@ module ActiveEntity
|
|
172
178
|
# === Concurrency and integrity
|
173
179
|
#
|
174
180
|
# Using this validation method in conjunction with
|
175
|
-
# {
|
181
|
+
# {ActiveEntity::Base#save}[rdoc-ref:Persistence#save]
|
176
182
|
# does not guarantee the absence of duplicate record insertions, because
|
177
183
|
# uniqueness checks on the application level are inherently prone to race
|
178
184
|
# conditions. For example, suppose that two users try to post a Comment at
|
@@ -212,7 +218,7 @@ module ActiveEntity
|
|
212
218
|
# the field's uniqueness.
|
213
219
|
#
|
214
220
|
# When the database catches such a duplicate insertion,
|
215
|
-
# {
|
221
|
+
# {ActiveEntity::Base#save}[rdoc-ref:Persistence#save] will raise an ActiveEntity::StatementInvalid
|
216
222
|
# exception. You can either choose to let this error propagate (which
|
217
223
|
# will result in the default Rails exception page being shown), or you
|
218
224
|
# can catch it and restart the transaction (e.g. by telling the user
|
@@ -220,17 +226,17 @@ module ActiveEntity
|
|
220
226
|
# This technique is also known as
|
221
227
|
# {optimistic concurrency control}[https://en.wikipedia.org/wiki/Optimistic_concurrency_control].
|
222
228
|
#
|
223
|
-
# The bundled
|
229
|
+
# The bundled ActiveEntity::ConnectionAdapters distinguish unique index
|
224
230
|
# constraint errors from other types of database errors by throwing an
|
225
|
-
#
|
231
|
+
# ActiveEntity::RecordNotUnique exception. For other adapters you will
|
226
232
|
# have to parse the (database-specific) exception message to detect such
|
227
233
|
# a case.
|
228
234
|
#
|
229
|
-
# The following bundled adapters throw the
|
235
|
+
# The following bundled adapters throw the ActiveEntity::RecordNotUnique exception:
|
230
236
|
#
|
231
|
-
# *
|
232
|
-
# *
|
233
|
-
# *
|
237
|
+
# * ActiveEntity::ConnectionAdapters::Mysql2Adapter.
|
238
|
+
# * ActiveEntity::ConnectionAdapters::SQLite3Adapter.
|
239
|
+
# * ActiveEntity::ConnectionAdapters::PostgreSQLAdapter.
|
234
240
|
def validates_uniqueness_on_active_record_of(*attr_names)
|
235
241
|
validates_with UniquenessOnActiveRecordValidator, _merge_attributes(attr_names)
|
236
242
|
end
|