activeentity 0.0.1.beta14 → 0.0.1.beta15
Sign up to get free protection for your applications and to get access to all the features.
- 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
|