@@ -15,32 +15,110 @@ module Validatable
15
15
#
16
16
# validates_associated :name, :addresses
17
17
# end
18
- class AssociatedValidator < ActiveModel ::EachValidator
18
+ class AssociatedValidator < ActiveModel ::Validator
19
+ # Required by `validates_with` so that the validator
20
+ # gets added to the correct attributes.
21
+ def attributes
22
+ options [ :attributes ]
23
+ end
19
24
20
- # Validates that the associations provided are either all nil or all
21
- # valid. If neither is true then the appropriate errors will be added to
22
- # the parent document.
25
+ # Checks that the named associations of the given record
26
+ # (`attributes`) are valid. This does NOT load the associations
27
+ # from the database, and will only validate records that are dirty
28
+ # or unpersisted.
23
29
#
24
- # @example Validate the association.
25
- # validator.validate_each(document, :name, name)
30
+ # If anything is not valid, appropriate errors will be added to
31
+ # the `document` parameter.
32
+ #
33
+ # @param [ Mongoid::Document ] document the document with the
34
+ # associations to validate.
35
+ def validate ( document )
36
+ options [ :attributes ] . each do |attr_name |
37
+ validate_association ( document , attr_name )
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # Validates that the given association provided is either nil,
44
+ # persisted and unchanged, or invalid. Otherwise, the appropriate errors
45
+ # will be added to the parent document.
26
46
#
27
47
# @param [ Document ] document The document to validate.
28
48
# @param [ Symbol ] attribute The association to validate.
29
- # @param [ Object ] value The value of the association.
30
- def validate_each ( document , attribute , value )
31
- begin
32
- document . begin_validate
33
- valid = Array . wrap ( value ) . collect do |doc |
34
- if doc . nil? || doc . flagged_for_destroy?
35
- true
49
+ def validate_association ( document , attribute )
50
+ # grab the proxy from the instance variable directly; we don't want
51
+ # any loading logic to run; we just want to see if it's already
52
+ # been loaded.
53
+ proxy = document . ivar ( attribute )
54
+ return unless proxy
55
+
56
+ # if the variable exists, now we see if it is a proxy, or an actual
57
+ # document. It might be a literal document instead of a proxy if this
58
+ # document was created with a Document instance as a provided attribute,
59
+ # e.g. "Post.new(message: Message.new)".
60
+ target = proxy . respond_to? ( :_target ) ? proxy . _target : proxy
61
+
62
+ # Now, fetch the list of documents from the target. Target may be a
63
+ # single value, or a list of values, and in the case of HasMany,
64
+ # might be a rather complex collection. We need to do this without
65
+ # triggering a load, so it's a bit of a delicate dance.
66
+ list = get_target_documents ( target )
67
+
68
+ valid = document . validating do
69
+ # Now, treating the target as an array, look at each element
70
+ # and see if it is valid, but only if it has already been
71
+ # persisted, or changed, and hasn't been flagged for destroy.
72
+ list . all? do |value |
73
+ if value && !value . flagged_for_destroy? && ( !value . persisted? || value . changed? )
74
+ value . validated? ? true : value . valid?
36
75
else
37
- doc . validated? ? true : doc . valid?
76
+ true
38
77
end
39
- end . all?
40
- ensure
41
- document . exit_validate
78
+ end
79
+ end
80
+
81
+ document . errors . add ( attribute , :invalid ) unless valid
82
+ end
83
+
84
+ private
85
+
86
+ # Examine the given target object and return an array of
87
+ # documents (possibly empty) that the target represents.
88
+ #
89
+ # @param [ Array | Mongoid::Document | Mongoid::Association::Proxy | HasMany::Enumerable ] target
90
+ # the target object to examine.
91
+ #
92
+ # @return [ Array<Mongoid::Document> ] the list of documents
93
+ def get_target_documents ( target )
94
+ if target . respond_to? ( :_loaded? )
95
+ get_target_documents_for_has_many ( target )
96
+ else
97
+ get_target_documents_for_other ( target )
42
98
end
43
- document . errors . add ( attribute , :invalid , **options ) unless valid
99
+ end
100
+
101
+ # Returns the list of all currently in-memory values held by
102
+ # the target. The target will not be loaded.
103
+ #
104
+ # @param [ HasMany::Enumerable ] target the target that will
105
+ # be examined for in-memory documents.
106
+ #
107
+ # @return [ Array<Mongoid::Document> ] the in-memory documents
108
+ # held by the target.
109
+ def get_target_documents_for_has_many ( target )
110
+ [ *target . _loaded . values , *target . _added . values ]
111
+ end
112
+
113
+ # Returns the target as an array. If the target represents a single
114
+ # value, it is wrapped in an array.
115
+ #
116
+ # @param [ Array | Mongoid::Document | Mongoid::Association::Proxy ] target
117
+ # the target to return.
118
+ #
119
+ # @return [ Array<Mongoid::Document> ] the target, as an array.
120
+ def get_target_documents_for_other ( target )
121
+ Array . wrap ( target )
44
122
end
45
123
end
46
124
end
0 commit comments