-
Notifications
You must be signed in to change notification settings - Fork 277
/
association.rb
284 lines (229 loc) · 9.55 KB
/
association.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
require 'active_support/inflector/inflections'
require 'neo4j/class_arguments'
module Neo4j
module ActiveNode
module HasN
class Association
include Neo4j::Shared::RelTypeConverters
include Neo4j::ActiveNode::Dependent::AssociationMethods
include Neo4j::ActiveNode::HasN::AssociationCypherMethods
attr_reader :type, :name, :relationship, :direction, :dependent, :model_class
def initialize(type, direction, name, options = {type: nil})
validate_init_arguments(type, direction, name, options)
@type = type.to_sym
@name = name
@direction = direction.to_sym
@target_class_name_from_name = name.to_s.classify
apply_vars_from_options(options)
end
def derive_model_class
refresh_model_class! if pending_model_refresh?
return @model_class unless @model_class.nil?
return nil if relationship_class.nil?
dir_class = direction == :in ? :from_class : :to_class
return false if relationship_class.send(dir_class).to_s.to_sym == :any
relationship_class.send(dir_class)
end
def refresh_model_class!
@pending_model_refresh = @target_classes_or_nil = nil
# Using #to_s on purpose here to take care of classes/strings/symbols
@model_class = ClassArguments.constantize_argument(@model_class.to_s) if @model_class
end
def queue_model_refresh!
@pending_model_refresh = true
end
def target_class_option(model_class)
case model_class
when nil
@target_class_name_from_name ? "#{association_model_namespace}::#{@target_class_name_from_name}" : @target_class_name_from_name
when Array
model_class.map { |sub_model_class| target_class_option(sub_model_class) }
when false
false
else
model_class.to_s[0, 2] == '::' ? model_class.to_s : "::#{model_class}"
end
end
def pending_model_refresh?
!!@pending_model_refresh
end
def target_class_names
option = target_class_option(derive_model_class)
@target_class_names ||= if option.is_a?(Array)
option.map(&:to_s)
elsif option
[option.to_s]
end
end
def inverse_of?(other)
origin_association == other
end
def target_classes
ClassArguments.constantize_argument(target_class_names)
end
def target_classes_or_nil
@target_classes_or_nil ||= discovered_model if target_class_names
end
def target_where_clause
return if model_class == false
Array.new(target_classes).map do |target_class|
"#{name}:`#{target_class.mapped_label_name}`"
end.join(' OR ')
end
def discovered_model
target_classes.select do |constant|
constant.ancestors.include?(::Neo4j::ActiveNode)
end
end
def target_class
return @target_class if @target_class
return if !(target_class_names && target_class_names.size == 1)
class_const = ClassArguments.constantize_argument(target_class_names[0])
@target_class = class_const
end
def callback(type)
@callbacks[type]
end
def perform_callback(caller, other_node, type)
return if callback(type).nil?
caller.send(callback(type), other_node)
end
def relationship_type(create = false)
case
when relationship_class
relationship_class_type
when !@relationship_type.nil?
@relationship_type
when @origin
origin_type
else
(create || exceptional_target_class?) && decorated_rel_type(@name)
end
end
attr_reader :relationship_class_name
def relationship_class_type
relationship_class._type.to_sym
end
def relationship_class
@relationship_class ||= @relationship_class_name && @relationship_class_name.constantize
end
def unique?
return relationship_class.unique? if rel_class?
@origin ? origin_association.unique? : !!@unique
end
def creates_unique_option
@unique || :none
end
def create_method
unique? ? :create_unique : :create
end
def _create_relationship(start_object, node_or_nodes, properties)
RelFactory.create(start_object, node_or_nodes, properties, self)
end
def relationship_class?
!!relationship_class
end
alias rel_class? relationship_class?
private
def association_model_namespace
Neo4j::Config.association_model_namespace_string
end
def get_direction(create, reverse = false)
dir = (create && @direction == :both) ? :out : @direction
if reverse
case dir
when :in then :out
when :out then :in
else :both
end
else
dir
end
end
def origin_association
target_class.associations[@origin]
end
def origin_type
origin_association.relationship_type
end
private
def apply_vars_from_options(options)
@relationship_class_name = options[:rel_class] && options[:rel_class].to_s
@relationship_type = options[:type] && options[:type].to_sym
@model_class = options[:model_class]
@callbacks = {before: options[:before], after: options[:after]}
@origin = options[:origin] && options[:origin].to_sym
@dependent = options[:dependent].try(:to_sym)
@unique = options[:unique]
end
# Return basic details about association as declared in the model
# @example
# has_many :in, :bands, type: :has_band
def base_declaration
"#{type} #{direction.inspect}, #{name.inspect}"
end
def validate_init_arguments(type, direction, name, options)
validate_association_options!(name, options)
validate_option_combinations(options)
validate_dependent(options[:dependent].try(:to_sym))
check_valid_type_and_dir(type, direction)
end
VALID_ASSOCIATION_OPTION_KEYS = [:type, :origin, :model_class, :rel_class, :dependent, :before, :after, :unique]
def validate_association_options!(_association_name, options)
ClassArguments.validate_argument!(options[:model_class], 'model_class')
ClassArguments.validate_argument!(options[:rel_class], 'rel_class')
message = case
when (message = type_keys_error_message(options.keys))
message
when !(unknown_keys = options.keys - VALID_ASSOCIATION_OPTION_KEYS).empty?
"Unknown option(s) specified: #{unknown_keys.join(', ')}"
end
fail ArgumentError, message if message
end
def type_keys_error_message(keys)
type_keys = (keys & [:type, :origin, :rel_class])
if type_keys.size > 1
"Only one of 'type', 'origin', or 'rel_class' options are allowed for associations"
elsif type_keys.empty?
"The 'type' option must be specified( even if it is `nil`) or `origin`/`rel_class` must be specified"
end
end
def check_valid_type_and_dir(type, direction)
fail ArgumentError, "Invalid association type: #{type.inspect} (valid value: :has_many and :has_one)" if ![:has_many, :has_one].include?(type.to_sym)
fail ArgumentError, "Invalid direction: #{direction.inspect} (valid value: :out, :in, and :both)" if ![:out, :in, :both].include?(direction.to_sym)
end
def validate_option_combinations(options)
[[:type, :origin],
[:type, :rel_class],
[:origin, :rel_class]].each do |key1, key2|
if options[key1] && options[key2]
fail ArgumentError, "Cannot specify both :#{key1} and :#{key2} (#{base_declaration})"
end
end
end
# Determine if model class as derived from the association name would be different than the one specified via the model_class key
# @example
# has_many :friends # Would return false
# has_many :friends, model_class: Friend # Would return false
# has_many :friends, model_class: Person # Would return true
def exceptional_target_class?
# TODO: Exceptional if target_class.nil?? (when model_class false)
target_class && target_class.name != @target_class_name_from_name
end
def validate_origin!
return if not @origin
association = origin_association
message = case
when !target_class
'Cannot use :origin without a model_class (implied or explicit)'
when !association
"Origin `#{@origin.inspect}` association not found for #{target_class} (specified in #{base_declaration})"
when @direction == association.direction
"Origin `#{@origin.inspect}` (specified in #{base_declaration}) has same direction `#{@direction}`)"
end
fail ArgumentError, message if message
end
end
end
end
end