activefacts-api 0.9.3 → 0.9.4

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.
@@ -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