activefacts-api 0.9.3 → 0.9.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -18,8 +18,8 @@ module ActiveFacts
18
18
 
19
19
  # Each ObjectType maintains a list of the Roles it plays:
20
20
  def roles(role_name = nil)
21
- unless instance_variable_defined? "@roles"
22
- @roles = RoleCollection.new # Initialize and extend without warnings.
21
+ unless instance_variable_defined? "@roles" # Avoid "instance variable not defined" warning from ||=
22
+ @roles = RoleCollection.new
23
23
  end
24
24
  case role_name
25
25
  when nil
@@ -65,7 +65,7 @@ module ActiveFacts
65
65
  # * :restrict - a list of values or ranges which this role may take. Not used yet.
66
66
  def has_one(role_name, options = {})
67
67
  role_name, related, mandatory, related_role_name = extract_binary_params(false, role_name, options)
68
- detect_fact_type_collision(:type => :has_one, :role => role_name, :related => related)
68
+ check_identifying_role_has_valid_cardinality(:has_one, role_name)
69
69
  define_binary_fact_type(false, role_name, related, mandatory, related_role_name)
70
70
  end
71
71
 
@@ -82,20 +82,20 @@ module ActiveFacts
82
82
  def one_to_one(role_name, options = {})
83
83
  role_name, related, mandatory, related_role_name =
84
84
  extract_binary_params(true, role_name, options)
85
- detect_fact_type_collision(:type => :one_to_one, :role => role_name, :related => related)
85
+ check_identifying_role_has_valid_cardinality(:one_to_one, role_name)
86
86
  define_binary_fact_type(true, role_name, related, mandatory, related_role_name)
87
87
  end
88
88
 
89
- def detect_fact_type_collision(fact)
90
- if respond_to?(:identifying_role_names) && identifying_role_names.include?(fact[:role])
91
- case fact[:type]
89
+ def check_identifying_role_has_valid_cardinality(type, role)
90
+ if is_entity_type && identifying_role_names.include?(role)
91
+ case type
92
92
  when :has_one
93
93
  if identifying_role_names.size == 1
94
- raise "Entity type #{self} cannot be identified by a single role '#{fact[:role]}' unless that role is one_to_one"
94
+ raise "Entity type #{self} cannot be identified by a single role '#{role}' unless that role is one_to_one"
95
95
  end
96
96
  when :one_to_one
97
97
  if identifying_role_names.size > 1
98
- raise "Entity type #{self} cannot be identified by a single role '#{fact[:role]}' unless that role is has_one"
98
+ raise "Entity type #{self} cannot be identified by a single role '#{role}' unless that role is has_one"
99
99
  end
100
100
  end
101
101
  end
@@ -108,51 +108,47 @@ module ActiveFacts
108
108
  # Without parameters, it returns the array of ObjectType supertypes
109
109
  # (one by Ruby inheritance, any others as defined using this method)
110
110
  def supertypes(*object_types)
111
- class_eval do
112
- @supertypes ||= []
113
- all_supertypes = supertypes_transitive
114
- object_types.each do |object_type|
115
- next if all_supertypes.include? object_type
116
- supertype =
117
- case object_type
118
- when Class
119
- object_type
120
- when Symbol
121
- # No late binding here:
122
- (object_type = vocabulary.const_get(object_type.to_s.camelcase))
123
- else
124
- raise "Illegal supertype #{object_type.inspect} for #{self.class.basename}"
125
- end
126
- raise "#{supertype.name} must be an object type in #{vocabulary.name}" unless supertype.respond_to?(:vocabulary) and supertype.vocabulary == self.vocabulary
127
-
128
- if is_entity_type != supertype.is_entity_type
129
- raise "#{self} < #{supertype}: A value type may not be a supertype of an entity type, and vice versa"
130
- end
131
-
132
- @supertypes << supertype
133
-
134
- # Realise the roles (create accessors) of this supertype.
135
- # REVISIT: The existing accessors at the other end will need to allow this class as role counterpart
136
- # REVISIT: Need to check all superclass roles recursively, unless we hit a common supertype
137
- realise_supertypes(object_type, all_supertypes)
138
- end
139
- [(superclass.respond_to?(:vocabulary) ? superclass : nil), *@supertypes].compact
140
- end
111
+ @supertypes ||= []
112
+ all_supertypes = supertypes_transitive
113
+ object_types.each do |object_type|
114
+ next if all_supertypes.include? object_type
115
+ supertype =
116
+ case object_type
117
+ when Class
118
+ object_type
119
+ when Symbol
120
+ # No late binding here:
121
+ (object_type = vocabulary.const_get(object_type.to_s.camelcase))
122
+ else
123
+ raise "Illegal supertype #{object_type.inspect} for #{self.class.basename}"
124
+ end
125
+ raise "#{supertype.name} must be an object type in #{vocabulary.name}" unless supertype.respond_to?(:vocabulary) and supertype.vocabulary == self.vocabulary
126
+
127
+ if is_entity_type != supertype.is_entity_type
128
+ raise "#{self} < #{supertype}: A value type may not be a supertype of an entity type, and vice versa"
129
+ end
130
+
131
+ @supertypes << supertype
132
+
133
+ # Realise the roles (create accessors) of this supertype.
134
+ # REVISIT: The existing accessors at the other end will need to allow this class as role counterpart
135
+ # REVISIT: Need to check all superclass roles recursively, unless we hit a common supertype
136
+ realise_supertypes(object_type, all_supertypes)
137
+ end
138
+ [(superclass.respond_to?(:vocabulary) ? superclass : nil), *@supertypes].compact
141
139
  end
142
140
 
143
141
  # Return the array of all ObjectType supertypes, transitively.
144
142
  def supertypes_transitive
145
- class_eval do
146
- supertypes = []
147
- v = superclass.respond_to?(:vocabulary) ? superclass.vocabulary : nil
148
- supertypes << superclass if v.kind_of?(Module)
149
- supertypes += (@supertypes ||= [])
150
- sts = supertypes.inject([]) do |a, t|
151
- next if a.include?(t)
152
- a += [t] + t.supertypes_transitive
153
- end.uniq
154
- sts # The local variable unconfuses rcov
155
- end
143
+ supertypes = []
144
+ v = superclass.respond_to?(:vocabulary) ? superclass.vocabulary : nil
145
+ supertypes << superclass if v.kind_of?(Module)
146
+ supertypes += (@supertypes ||= [])
147
+ sts = supertypes.inject([]) do |a, t|
148
+ next if a.include?(t)
149
+ a += [t] + t.supertypes_transitive
150
+ end.uniq
151
+ sts # The local variable unconfuses rcov
156
152
  end
157
153
 
158
154
  def subtypes
@@ -175,10 +171,6 @@ module ActiveFacts
175
171
  end
176
172
  end
177
173
 
178
- def is_a? klass
179
- super || supertypes_transitive.include?(klass)
180
- end
181
-
182
174
  private
183
175
 
184
176
  def realise_supertypes(object_type, all_supertypes = nil)
@@ -220,118 +212,130 @@ module ActiveFacts
220
212
  end
221
213
 
222
214
  def define_unary_role_accessor(role)
223
- class_eval do
224
- define_method role.setter do |value|
225
- assigned = case value
226
- when nil; nil
227
- when false; false
228
- else true
229
- end
230
- instance_variable_set(role.variable, assigned)
231
- # REVISIT: Provide a way to find all instances playing/not playing this role
232
- # Analogous to true.all_thing_as_role_name...
233
- assigned
234
- end
235
- end
215
+ define_method role.setter do |value|
216
+ assigned = case value
217
+ when nil; nil
218
+ when false; false
219
+ else true
220
+ end
221
+ instance_variable_set(role.variable, assigned)
222
+ # REVISIT: Provide a way to find all instances playing/not playing this role
223
+ # Analogous to true.all_thing_as_role_name...
224
+ assigned
225
+ end
236
226
  define_single_role_getter(role)
237
227
  end
238
228
 
239
229
  def define_single_role_getter(role)
240
- class_eval do
241
- define_method role.getter do |*a|
242
- raise "Parameters passed to #{self.class.name}\##{role.name}" if a.size > 0
243
- instance_variable_get(role.variable)
244
- end
245
- end
230
+ define_method role.getter do |*a|
231
+ raise "Parameters passed to #{self.class.name}\##{role.name}" if a.size > 0
232
+ instance_variable_get(role.variable)
233
+ end
246
234
  end
247
235
 
248
236
  def define_one_to_one_accessor(role)
249
237
  define_single_role_getter(role)
250
238
 
251
- class_eval do
252
- define_method role.setter do |value|
253
-
254
- old = instance_variable_get(role.variable)
255
- return true if old.equal?(value) # Occurs when another instance having the same value is assigned
256
-
257
- value = role.adapt(@constellation, value) if value
258
- return true if old.equal?(value) # Occurs when same value but not same instance is assigned
259
-
260
- detect_inconsistencies(role, value)
261
-
262
- if @constellation && old
263
- keys = old.related_entities.map do |entity|
264
- [entity.identifying_role_values, entity]
265
- end
266
- end
267
-
268
- instance_variable_set(role.variable, value)
269
-
270
- # Remove self from the old counterpart:
271
- old.send(role.counterpart.setter, nil) if old
272
-
273
- # Assign self to the new counterpart
274
- value.send(role.counterpart.setter, self) if value
275
-
276
- if keys
277
- keys.each do |key, entity|
278
- entity.instance_index.refresh_key(key)
279
- end
280
- end
281
-
282
- value
283
- end
284
- end
239
+ define_method role.setter do |value|
240
+ old = instance_variable_get(role.variable)
241
+
242
+ # When exactly the same value instance is assigned, we're done:
243
+ return true if old == value
244
+
245
+ if value and o = role.counterpart.object_type and (!value.is_a?(o) || value.constellation != @constellation)
246
+ value = @constellation.assert(o, *Array(value))
247
+ # return true if old.equal?(value) # Occurs when same value but not same instance is assigned
248
+ return true if old == value
249
+ end
250
+
251
+ dependent_entities = nil
252
+ if (role.is_identifying)
253
+ check_value_change_legality(role, value)
254
+
255
+ # We're changing this object's key.
256
+ # Find all object which are identified by this object, and save their old keys
257
+ if @constellation && old
258
+ dependent_entities = old.related_entities.map do |entity|
259
+ [entity.identifying_role_values, entity]
260
+ end
261
+ end
262
+ end
263
+
264
+ instance_variable_set(role.variable, value)
265
+
266
+ # Remove self from the old counterpart:
267
+ old.send(role.counterpart.setter, nil) if old
268
+
269
+ @constellation.when_admitted do
270
+ # Assign self to the new counterpart
271
+ value.send(role.counterpart.setter, self) if value
272
+
273
+ # Propagate dependent key changes
274
+ if dependent_entities
275
+ dependent_entities.each do |old_key, entity|
276
+ entity.instance_index.refresh_key(old_key)
277
+ end
278
+ end
279
+ end
280
+
281
+ value
282
+ end
285
283
  end
286
284
 
287
285
  def define_one_to_many_accessor(role)
288
286
  define_single_role_getter(role)
289
287
 
290
- class_eval do
291
- define_method role.setter do |value|
292
- role_var = role.variable
293
-
294
- # Get old value, and jump out early if it's unchanged:
295
- old = instance_variable_get(role_var)
296
- return value if old.equal?(value) # Occurs during one_to_one assignment, for example
297
-
298
- value = role.adapt(constellation, value) if value
299
- return value if old.equal?(value) # Occurs when another instance having the same value is assigned
300
-
301
- detect_inconsistencies(role, value) if value
302
-
303
- if old && old.constellation
304
- keys = old.related_entities.map do |entity|
305
- [entity.identifying_role_values, entity]
306
- end
307
- end
308
-
309
- instance_variable_set(role_var, value)
310
-
311
- # Remove "self" from the old counterpart:
312
- old.send(getter = role.counterpart.getter).update(self, nil) if old
313
-
314
- # Add "self" into the counterpart
315
- value.send(getter ||= role.counterpart.getter).update(old, self) if value
316
-
317
- if keys
318
- keys.each do |key, entity|
319
- entity.instance_index.refresh_key(key)
320
- end
321
- end
322
-
323
- value
324
- end
325
- end
288
+ define_method role.setter do |value|
289
+ role_var = role.variable
290
+
291
+ # Get old value, and jump out early if it's unchanged:
292
+ old = instance_variable_get(role_var)
293
+ return value if old.equal?(value) # Occurs during one_to_one assignment, for example
294
+
295
+ # assert a new instance for the role value if necessary
296
+ if value and o = role.counterpart.object_type and (!value.is_a?(o) || value.constellation != @constellation)
297
+ value = @constellation.assert(o, *Array(value))
298
+ return value if old.equal?(value) # Occurs when another instance having the same value is assigned
299
+ end
300
+
301
+ dependent_entities = nil
302
+ if (role.is_identifying)
303
+ check_value_change_legality(role, value) if value
304
+
305
+ if old && old.constellation
306
+ # If our identity has changed and we identify others, prepare to reindex them
307
+ dependent_entities = old.related_entities.map do |entity|
308
+ [entity.identifying_role_values, entity]
309
+ end
310
+ end
311
+ end
312
+
313
+ instance_variable_set(role_var, value)
314
+
315
+ # Remove "self" from the old counterpart:
316
+ old.send(getter = role.counterpart.getter).update(self, nil) if old
317
+
318
+ @constellation.when_admitted do
319
+ # REVISIT: Delay co-referencing here if the object is still a candidate
320
+ # Add "self" into the counterpart
321
+ value.send(getter ||= role.counterpart.getter).update(old, self) if value
322
+
323
+ if dependent_entities
324
+ dependent_entities.each do |key, entity|
325
+ entity.instance_index.refresh_key(key)
326
+ end
327
+ end
328
+ end
329
+
330
+ value
331
+ end
326
332
  end
327
333
 
328
334
  def define_many_to_one_accessor(role)
329
- class_eval do
330
- define_method role.getter do
331
- role_var = role.variable
332
- instance_variable_get(role_var) or
333
- instance_variable_set(role_var, RoleValues.new)
334
- end
335
+ define_method role.getter do
336
+ role_var = role.variable
337
+ instance_variable_get(role_var) or
338
+ instance_variable_set(role_var, RoleValues.new)
335
339
  end
336
340
  end
337
341
 
@@ -375,7 +379,7 @@ module ActiveFacts
375
379
  role_name = (Class === role_name ? a.name.snakecase : role_name).to_sym
376
380
 
377
381
  # The related class might be forward-referenced, so handle a Symbol/String instead of a Class.
378
- related_name = options.delete(:class)
382
+ specified_class = related_name = options.delete(:class)
379
383
  case related_name
380
384
  when nil
381
385
  related = role_name # No :class provided, assume it matches the role_name
@@ -412,7 +416,7 @@ module ActiveFacts
412
416
 
413
417
  # Avoid a confusing mismatch:
414
418
  # Note that if you have a role "supervisor" and a sub-class "Supervisor", this'll bitch.
415
- if (Class === related && (indicated = vocabulary.object_type(role_name)) && indicated != related)
419
+ if (!specified_class && Class === related && (indicated = vocabulary.object_type(role_name)) && indicated != related)
416
420
  raise "Role name #{role_name} indicates a different counterpart object_type #{indicated} than specified"
417
421
  end
418
422
 
@@ -55,42 +55,13 @@ module ActiveFacts
55
55
  counterpart == nil
56
56
  end
57
57
 
58
- def is_inherited?(klass)
59
- klass.supertypes_transitive.include?(@object_type)
60
- end
61
-
62
- def counterpart_object_type
63
- # This method is sometimes used when unaries are used in an entity's identifier.
64
- @is_unary ? TrueClass : (counterpart ? counterpart.object_type : nil)
65
- end
66
-
67
58
  def inspect
68
59
  "<Role #{object_type.name}.#{name}>"
69
60
  end
70
61
 
71
- def adapt(constellation, value) #:nodoc:
72
- # If the value is a compatible class, use it (if in another constellation, clone it),
73
- # else create a compatible object using the value as constructor parameters.
74
- if value.is_a?(counterpart.object_type)
75
- # Check that the value is in a compatible constellation, clone if not:
76
- if constellation && (vc = value.constellation) && vc != constellation
77
- # Cross-constellation assignment!
78
- # Just take the identifying_role_values to make a new object
79
- value = constellation.send(value.class.basename, value.identifying_role_values)
80
- end
81
- value.constellation = constellation if constellation
82
- else
83
- value = [value] unless Array === value
84
- raise "No parameters were provided to identify an #{counterpart.object_type.basename} instance" if value == []
85
- if constellation
86
- value = constellation.send(counterpart.object_type.basename.to_sym, *value)
87
- else
88
- #trace :assert, "Constructing new #{counterpart.object_type} with #{value.inspect}" do
89
- value = counterpart.object_type.new(*value)
90
- #end
91
- end
92
- end
93
- value
62
+ def verbalise
63
+ "Role #{name} of #{object_type}, " +
64
+ (@is_unary ? 'unary' : (counterpart ? 'played by' + counterpart.object_type : 'undefined'))
94
65
  end
95
66
 
96
67
  private
@@ -7,8 +7,8 @@
7
7
  # These extensions add ActiveFacts ObjectType and Instance behaviour into base Ruby value classes,
8
8
  # and allow any Class to become an Entity.
9
9
  #
10
- require 'date'
11
10
  require 'activefacts/api/numeric'
11
+ require 'activefacts/api/date'
12
12
  require 'activefacts/api/guid'
13
13
 
14
14
  module ActiveFacts
@@ -16,6 +16,7 @@ module ActiveFacts
16
16
  # Adapter module to add value_type to all potential value classes
17
17
  module ValueClass #:nodoc:
18
18
  def value_type *args, &block #:nodoc:
19
+ # The inclusion of instance methods triggers ClassMethods to be included in the class too
19
20
  include ActiveFacts::API::Value
20
21
  value_type(*args, &block)
21
22
  end
@@ -29,22 +30,23 @@ ValueClasses.each{|c|
29
30
  c.send :extend, ActiveFacts::API::ValueClass
30
31
  }
31
32
 
33
+ # Cannot subclass or delegate True, False or nil, so inject the required behaviour
32
34
  class TrueClass #:nodoc:
33
35
  def verbalise(role_name = nil); role_name ? "#{role_name}: true" : "true"; end
34
36
  def identifying_role_values; self; end
35
- def self.identifying_role_values(*a); true; end
37
+ def self.identifying_role_values(*a); a.replace([{}]); true end
36
38
  end
37
39
 
38
40
  class FalseClass #:nodoc:
39
41
  def verbalise(role_name = nil); role_name ? "#{role_name}: false" : "false"; end
40
42
  def identifying_role_values; self; end
41
- def self.identifying_role_values(*a); false; end
43
+ def self.identifying_role_values(*a); a.replace([{}]); false end
42
44
  end
43
45
 
44
46
  class NilClass #:nodoc:
45
47
  def verbalise; "nil"; end
46
48
  def identifying_role_values; self; end
47
- def self.identifying_role_values(*a); nil; end
49
+ def self.identifying_role_values(*a); a.replace([{}]); nil end
48
50
  end
49
51
 
50
52
  class Class
@@ -52,6 +54,8 @@ class Class
52
54
  # The parameters are the names (Symbols) of the identifying roles.
53
55
  def identified_by *args, &b
54
56
  raise "#{basename} is not an entity type" if respond_to? :value_type # Don't make a ValueType into an EntityType
57
+
58
+ # The inclusion of instance methods triggers ClassMethods to be included in the class too
55
59
  include ActiveFacts::API::Entity
56
60
  identified_by(*args, &b)
57
61
  end