activefacts-api 0.8.9 → 0.8.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -15,21 +15,23 @@ module ActiveFacts
15
15
 
16
16
  def initialize(args = []) #:nodoc:
17
17
  unless (self.class.is_entity_type)
18
- #if (self.class.superclass != Object)
19
- # puts "constructing #{self.class.superclass} with #{args.inspect}"
20
- super(*args)
18
+ begin
19
+ super(*args)
20
+ rescue ArgumentError => e
21
+ e.message << " constructing a #{self.class}"
22
+ raise
23
+ end
21
24
  end
22
25
  end
23
26
 
24
27
  # Verbalise this instance
25
28
  def verbalise
26
29
  # This method should always be overridden in subclasses
27
- raise "#{self.class} Instance verbalisation needed"
28
30
  end
29
31
 
30
32
  # De-assign all functional roles and remove from constellation, if any.
31
33
  def retract
32
- # Delete from the constellation first, so it can remember our identifying role values
34
+ # Delete from the constellation first, while it remembers our identifying role values
33
35
  @constellation.__retract(self) if @constellation
34
36
 
35
37
  # Now, for all roles (from this class and all supertypes), assign nil to all functional roles
@@ -37,12 +39,18 @@ module ActiveFacts
37
39
  ([self.class]+self.class.supertypes_transitive).each do |klass|
38
40
  klass.roles.each do |role_name, role|
39
41
  next if role.unary?
40
- next if !role.unique
41
-
42
42
  counterpart = role.counterpart
43
- puts "Nullifying mandatory role #{role.name} of #{role.owner.name}" if counterpart.mandatory
43
+ if role.unique
44
+ # puts "Nullifying mandatory role #{role.name} of #{role.object_type.name}" if counterpart.mandatory
44
45
 
45
- send "#{role.name}=", nil
46
+ send role.setter, nil
47
+ else
48
+ # puts "Not removing role #{role_name} from counterpart RoleValues #{counterpart.name}"
49
+ # Duplicate the array using to_a, as the RoleValues here will be modified as we traverse it:
50
+ send(role.name).to_a.each do |v|
51
+ v.send(counterpart.setter, nil)
52
+ end
53
+ end
46
54
  end
47
55
  end
48
56
  end
@@ -12,72 +12,73 @@ module ActiveFacts
12
12
  # arguments (where ObjectType is the object_type name you're interested in)
13
13
  #
14
14
  class InstanceIndex
15
+ def initialize(constellation, klass)
16
+ @constellation = constellation
17
+ @klass = klass
18
+ @hash = {}
19
+ end
20
+
21
+ def inspect
22
+ "<InstanceIndex for #{@klass.name} in #{@constellation.inspect}>"
23
+ end
24
+
25
+ def assert(*args)
26
+ #trace :assert, "Asserting #{@klass} with #{args.inspect}" do
27
+ instance, key = *@klass.assert_instance(@constellation, args)
28
+ instance
29
+ #end
30
+ end
31
+
32
+ def include?(*args)
33
+ if args.size == 1 && args[0].is_a?(@klass)
34
+ key = args[0].identifying_role_values
35
+ else
36
+ key = @klass.identifying_role_values(*args)
37
+ end
38
+ return @hash[key]
39
+ end
40
+
15
41
  def []=(key, value) #:nodoc:
16
- raise "Adding RoleProxy to InstanceIndex" if value && RoleProxy === value
17
- h[key] = value
42
+ @hash[key] = value
18
43
  end
19
44
 
20
45
  def [](*args)
21
- a = args
22
- #a = naked(args)
23
- # p "vvvv",
24
- # args,
25
- # a,
26
- # keys.map{|k| v=super(k); (RoleProxy === k ? "*" : "")+k.to_s+"=>"+(RoleProxy === v ? "*" : "")+v.to_s}*",",
27
- # "^^^^"
28
- h[*a]
29
- #super(*a)
46
+ @hash[*args]
30
47
  end
31
48
 
32
49
  def size
33
- h.size
50
+ @hash.size
34
51
  end
35
52
 
36
53
  def empty?
37
- h.size == 0
54
+ @hash.size == 0
38
55
  end
39
56
 
40
57
  def each &b
41
- h.each &b
58
+ @hash.each &b
42
59
  end
43
60
 
44
61
  def map &b
45
- h.map &b
62
+ @hash.map &b
46
63
  end
47
64
 
48
65
  def detect &b
49
- r = h.detect &b
66
+ r = @hash.detect &b
50
67
  r ? r[1] : nil
51
68
  end
52
69
 
53
70
  # Return an array of all the instances of this object_type
54
71
  def values
55
- h.values
72
+ @hash.values
56
73
  end
57
74
 
58
75
  # Return an array of the identifying role values arrays for all the instances of this object_type
59
76
  def keys
60
- h.keys
77
+ @hash.keys
61
78
  end
62
79
 
63
80
  def delete_if(&b) #:nodoc:
64
- h.delete_if &b
65
- end
66
-
67
- private
68
- def h
69
- @hash ||= {}
70
- end
71
-
72
- def naked(o)
73
- case o
74
- when Array
75
- o.map{|e| naked(e) }
76
- when RoleProxy
77
- o.__getobj__
78
- else
79
- o
80
- end
81
+ @hash.delete_if &b
81
82
  end
82
83
  end
83
84
  end
@@ -22,11 +22,15 @@ class Int < SimpleDelegator
22
22
  end
23
23
 
24
24
  def hash #:nodoc:
25
- __getobj__.hash ^ self.class.hash
25
+ __getobj__.hash
26
26
  end
27
27
 
28
28
  def eql?(o) #:nodoc:
29
- self.class == o.class and __getobj__.eql?(Integer(o))
29
+ # Note: This and #hash do not work the way you'd expect,
30
+ # and differently in each Ruby interpreter. If you store
31
+ # an Int or Real in a hash, you cannot reliably retrieve
32
+ # them with the corresponding Integer or Real.
33
+ __getobj__.eql?(Integer(o))
30
34
  end
31
35
 
32
36
  def ==(o) #:nodoc:
@@ -34,7 +38,7 @@ class Int < SimpleDelegator
34
38
  end
35
39
 
36
40
  def is_a?(k)
37
- __getobj__.is_a?(k)
41
+ __getobj__.is_a?(k) || super
38
42
  end
39
43
 
40
44
  def inspect
@@ -49,7 +53,7 @@ class Real < SimpleDelegator
49
53
  end
50
54
 
51
55
  def hash #:nodoc:
52
- __getobj__.hash ^ self.class.hash
56
+ __getobj__.hash
53
57
  end
54
58
 
55
59
  def to_s #:nodoc:
@@ -57,7 +61,8 @@ class Real < SimpleDelegator
57
61
  end
58
62
 
59
63
  def eql?(o) #:nodoc:
60
- self.class == o.class and __getobj__.eql?(Float(o))
64
+ # Note: See the note above on Int#eql?
65
+ __getobj__.eql?(Float(o))
61
66
  end
62
67
 
63
68
  def ==(o) #:nodoc:
@@ -65,7 +70,7 @@ class Real < SimpleDelegator
65
70
  end
66
71
 
67
72
  def is_a?(k)
68
- __getobj__.is_a?(k)
73
+ __getobj__.is_a?(k) || super
69
74
  end
70
75
 
71
76
  def inspect #:nodoc:
@@ -78,7 +83,6 @@ class ::Date
78
83
  class << self; alias_method :old_new, :new end
79
84
  # Date.new cannot normally be called passing a Date as the parameter. This allows that.
80
85
  def self.new(*a, &b)
81
- #puts "Constructing date with #{a.inspect} from #{caller*"\n\t"}"
82
86
  if (a.size == 1 && a[0].is_a?(Date))
83
87
  a = a[0]
84
88
  civil(a.year, a.month, a.day, a.start)
@@ -95,13 +99,12 @@ class ::DateTime
95
99
  class << self; alias_method :old_new, :new end
96
100
  # DateTime.new cannot normally be called passing a Date or DateTime as the parameter. This allows that.
97
101
  def self.new(*a, &b)
98
- #puts "Constructing DateTime with #{a.inspect} from #{caller*"\n\t"}"
99
102
  if (a.size == 1)
100
103
  a = a[0]
101
104
  if (DateTime === a)
102
105
  civil(a.year, a.month, a.day, a.hour, a.min, a.sec, a.start)
103
106
  elsif (Date === a)
104
- civil(a.year, a.month, a.day, a.start)
107
+ civil(a.year, a.month, a.day, 0, 0, 0, a.start)
105
108
  else
106
109
  civil(*a, &b)
107
110
  end
@@ -118,14 +121,13 @@ end
118
121
  # The assigned value will be filled out everywhere it needs to be, upon save.
119
122
  class AutoCounter
120
123
  def initialize(i = :new)
121
- raise "AutoCounter #{self.class} may not be #{i.inspect}" unless i == :new or i.is_a?(Integer)
122
- # puts "new AutoCounter #{self.class} from\n\t#{caller.select{|s| s !~ %r{rspec}}*"\n\t"}"
123
- @value = i == :new ? nil : i
124
+ raise "AutoCounter #{self.class} may not be #{i.inspect}" unless i == :new or i.is_a?(Integer) or i.is_a?(AutoCounter)
125
+ @value = i == :new ? nil : i.to_i
124
126
  end
125
127
 
126
128
  # Assign a definite value to an AutoCounter; this may only be done once
127
129
  def assign(i)
128
- raise ArgumentError if @value
130
+ raise ArgumentError, "Illegal attempt to assign integer value of a committed AutoCounter" if @value
129
131
  @value = i.to_i
130
132
  end
131
133
 
@@ -143,8 +145,14 @@ class AutoCounter
143
145
  end
144
146
 
145
147
  # An AutoCounter may only be used in numeric expressions after a definite value has been assigned
146
- def self.coerce(i)
147
- raise ArgumentError unless @value
148
+ def to_i
149
+ raise ArgumentError, "Illegal attempt to get integer value of an uncommitted AutoCounter" unless @value
150
+ @value
151
+ end
152
+
153
+ # Coerce "i" to be of the same type as self
154
+ def coerce(i)
155
+ raise ArgumentError, "Illegal attempt to use the value of an uncommitted AutoCounter" unless @value
148
156
  [ i.to_i, @value ]
149
157
  end
150
158
 
@@ -153,22 +161,26 @@ class AutoCounter
153
161
  end
154
162
 
155
163
  def hash #:nodoc:
156
- to_s.hash ^ self.class.hash
164
+ if self.defined?
165
+ @value.hash
166
+ else
167
+ 0
168
+ end
157
169
  end
158
170
 
159
171
  def eql?(o) #:nodoc:
160
- self.class == o.class and to_s.eql?(o.to_s)
172
+ to_s.eql?(o.to_s)
161
173
  end
162
174
 
163
175
  def self.inherited(other) #:nodoc:
164
176
  def other.identifying_role_values(*args)
165
177
  return nil if args == [:new] # A new object has no identifying_role_values
178
+ return args[0] if args.size == 1 and args[0].is_a?(AutoCounter)
166
179
  return new(*args)
167
180
  end
168
181
  super
169
182
  end
170
183
 
171
- private
172
184
  def clone
173
185
  raise "Not allowed to clone AutoCounters"
174
186
  end
@@ -17,27 +17,25 @@ module ActiveFacts
17
17
  end
18
18
 
19
19
  # Each ObjectType maintains a list of the Roles it plays:
20
- def roles(name = nil)
20
+ def roles(role_name = nil)
21
21
  unless instance_variable_defined? "@roles"
22
22
  @roles = RoleCollection.new # Initialize and extend without warnings.
23
23
  end
24
- case name
24
+ case role_name
25
25
  when nil
26
26
  @roles
27
27
  when Symbol, String
28
28
  # Search this class then all supertypes:
29
- unless role = @roles[name.to_sym]
29
+ unless role = @roles[role_name.to_sym]
30
30
  role = nil
31
31
  supertypes.each do |supertype|
32
- r = supertype.roles(name) rescue nil
32
+ r = supertype.roles(role_name) rescue nil
33
33
  next unless r
34
34
  role = r
35
35
  break
36
36
  end
37
37
  end
38
- raise "Role #{basename}.#{name} is not defined" unless role
39
- # Bind the role if possible, but don't require it:
40
- role.resolve_counterpart(vocabulary) rescue nil unless role.counterpart_object_type.is_a?(Class)
38
+ raise "Role #{basename}.#{role_name} is not defined" unless role
41
39
  role
42
40
  else
43
41
  nil
@@ -48,7 +46,7 @@ module ActiveFacts
48
46
  #
49
47
  # Example: maybe :is_ceo
50
48
  def maybe(role_name)
51
- realise_role(roles[role_name] = Role.new(self, TrueClass, nil, role_name))
49
+ realise_role(roles[role_name] = Role.new(self, nil, role_name))
52
50
  end
53
51
 
54
52
  # Define a binary fact type relating this object_type to another,
@@ -83,28 +81,38 @@ module ActiveFacts
83
81
  end
84
82
 
85
83
  # Access supertypes or add new supertypes; multiple inheritance.
86
- # With parameters (Class objects), it adds new supertypes to this class. Instances of this class will then have role methods for any new superclasses (transitively). Superclasses must be Ruby classes which are existing ObjectTypes.
87
- # Without parameters, it returns the array of ObjectType supertypes (one by Ruby inheritance, any others as defined using this method)
84
+ # With parameters (Class objects), it adds new supertypes to this class.
85
+ # Instances of this class will then have role methods for any new superclasses (transitively).
86
+ # Superclasses must be Ruby classes which are existing ObjectTypes.
87
+ # Without parameters, it returns the array of ObjectType supertypes
88
+ # (one by Ruby inheritance, any others as defined using this method)
88
89
  def supertypes(*object_types)
89
90
  class_eval do
90
91
  @supertypes ||= []
91
92
  all_supertypes = supertypes_transitive
92
93
  object_types.each do |object_type|
93
94
  next if all_supertypes.include? object_type
94
- case object_type
95
- when Class
96
- @supertypes << object_type
97
- when Symbol
98
- # No late binding here:
99
- @supertypes << (object_type = vocabulary.const_get(object_type.to_s.camelcase))
100
- else
101
- raise "Illegal supertype #{object_type.inspect} for #{self.class.basename}"
95
+ supertype =
96
+ case object_type
97
+ when Class
98
+ object_type
99
+ when Symbol
100
+ # No late binding here:
101
+ (object_type = vocabulary.const_get(object_type.to_s.camelcase))
102
+ else
103
+ raise "Illegal supertype #{object_type.inspect} for #{self.class.basename}"
104
+ end
105
+ raise "#{supertype.name} must be an object type in #{vocabulary.name}" unless supertype.respond_to?(:vocabulary) and supertype.vocabulary == self.vocabulary
106
+
107
+ if is_entity_type != supertype.is_entity_type
108
+ raise "#{self} < #{supertype}: A value type may not be a supertype of an entity type, and vice versa"
102
109
  end
103
110
 
111
+ @supertypes << supertype
112
+
104
113
  # Realise the roles (create accessors) of this supertype.
105
114
  # REVISIT: The existing accessors at the other end will need to allow this class as role counterpart
106
115
  # REVISIT: Need to check all superclass roles recursively, unless we hit a common supertype
107
- #puts "Realising object_type #{object_type.name} in #{basename}"
108
116
  realise_supertypes(object_type, all_supertypes)
109
117
  end
110
118
  [(superclass.vocabulary && superclass rescue nil), *@supertypes].compact
@@ -117,11 +125,12 @@ module ActiveFacts
117
125
  supertypes = []
118
126
  supertypes << superclass if Module === (superclass.vocabulary rescue nil)
119
127
  supertypes += (@supertypes ||= [])
120
- supertypes.inject([]) {|a, t|
121
- next if a.include?(t)
122
- a += [t]
123
- a += t.supertypes_transitive rescue []
124
- }.uniq
128
+ sts = supertypes.inject([]) do |a, t|
129
+ next if a.include?(t)
130
+ a += [t]
131
+ a += t.supertypes_transitive rescue []
132
+ end.uniq
133
+ sts # The local variable unconfuses rcov
125
134
  end
126
135
  end
127
136
 
@@ -131,20 +140,20 @@ module ActiveFacts
131
140
 
132
141
  # Every new role added or inherited comes through here:
133
142
  def realise_role(role) #:nodoc:
134
- #puts "Realising role #{role.counterpart_object_type.basename rescue role.counterpart_object_type}.#{role.name} in #{basename}"
135
-
136
143
  if (!role.counterpart)
137
144
  # Unary role
138
145
  define_unary_role_accessor(role)
139
146
  elsif (role.unique)
140
- define_single_role_accessor(role, role.counterpart.unique)
147
+ if role.counterpart.unique
148
+ define_one_to_one_accessor(role)
149
+ else
150
+ define_one_to_many_accessor(role)
151
+ end
141
152
  else
142
- define_array_role_accessor(role)
153
+ define_many_to_one_accessor(role)
143
154
  end
144
155
  end
145
156
 
146
- # REVISIT: Use method_missing to catch all_some_role_as_other_role_and_third_role, to sort_by those roles?
147
-
148
157
  def is_a? klass
149
158
  super || supertypes_transitive.include?(klass)
150
159
  end
@@ -154,14 +163,12 @@ module ActiveFacts
154
163
  def realise_supertypes(object_type, all_supertypes = nil)
155
164
  all_supertypes ||= supertypes_transitive
156
165
  s = object_type.supertypes
157
- #puts "realising #{object_type.basename} supertypes #{s.inspect} of #{basename}"
158
- s.each {|t|
159
- next if all_supertypes.include? t
160
- realise_supertypes(t, all_supertypes)
161
- t.subtypes << self
162
- all_supertypes << t
163
- }
164
- #puts "Realising roles of #{object_type.basename} in #{basename}"
166
+ s.each do |t|
167
+ next if all_supertypes.include? t
168
+ realise_supertypes(t, all_supertypes)
169
+ t.subtypes << self unless t.subtypes.include?(self)
170
+ all_supertypes << t
171
+ end
165
172
  realise_roles(object_type)
166
173
  end
167
174
 
@@ -174,34 +181,35 @@ module ActiveFacts
174
181
 
175
182
  # Shared code for both kinds of binary fact type (has_one and one_to_one)
176
183
  def define_binary_fact_type(one_to_one, role_name, related, mandatory, related_role_name)
177
- # puts "#{self}.#{role_name} is to #{related.inspect}, #{mandatory ? :mandatory : :optional}, related role is #{related_role_name}"
178
-
184
+ # REVISIT: What if the role exists on a supertype? This won't prevent that:
179
185
  raise "#{name} cannot have more than one role named #{role_name}" if roles[role_name]
180
- roles[role_name] = role = Role.new(self, related, nil, role_name, mandatory)
186
+ roles[role_name] = role = Role.new(self, nil, role_name, mandatory)
181
187
 
182
188
  # There may be a forward reference here where role_name is a Symbol,
183
189
  # and the block runs later when that Symbol is bound to the object_type.
184
190
  when_bound(related, self, role_name, related_role_name) do |target, definer, role_name, related_role_name|
185
191
  if (one_to_one)
186
- target.roles[related_role_name] = role.counterpart = Role.new(target, definer, role, related_role_name, false)
192
+ target.roles[related_role_name] = role.counterpart = Role.new(target, role, related_role_name, false)
187
193
  else
188
- target.roles[related_role_name] = role.counterpart = Role.new(target, definer, role, related_role_name, false, false)
194
+ target.roles[related_role_name] = role.counterpart = Role.new(target, role, related_role_name, false, false)
189
195
  end
190
- role.counterpart_object_type = target
191
- #puts "Realising role pair #{definer.basename}.#{role_name} <-> #{target.basename}.#{related_role_name}"
192
196
  realise_role(role)
193
197
  target.realise_role(role.counterpart)
194
198
  end
195
199
  end
196
200
 
197
201
  def define_unary_role_accessor(role)
198
- # puts "Defining #{basename}.#{role_name} as unary"
199
202
  class_eval do
200
- define_method "#{role.name}=" do |value|
201
- #puts "Setting #{self.class.name} #{object_id}.@#{role.name} to #{(value ? true : nil).inspect}"
202
- instance_variable_set("@#{role.name}", value ? true : nil)
203
+ define_method role.setter do |value|
204
+ assigned = case value
205
+ when nil; nil
206
+ when false; false
207
+ else true
208
+ end
209
+ instance_variable_set(role.variable, assigned)
203
210
  # REVISIT: Provide a way to find all instances playing/not playing this role
204
211
  # Analogous to true.all_thing_as_role_name...
212
+ assigned
205
213
  end
206
214
  end
207
215
  define_single_role_getter(role)
@@ -209,86 +217,90 @@ module ActiveFacts
209
217
 
210
218
  def define_single_role_getter(role)
211
219
  class_eval do
212
- define_method role.name do |*a|
220
+ define_method role.getter do |*a|
213
221
  raise "Parameters passed to #{self.class.name}\##{role.name}" if a.size > 0
214
- i = instance_variable_get("@#{role.name}") rescue nil
215
- i ? RoleProxy.new(role, i) : i
216
- i
222
+ instance_variable_get(role.variable) rescue nil
217
223
  end
218
224
  end
219
225
  end
220
226
 
221
- # REVISIT: Add __add_to(constellation) and __remove(constellation) here?
222
- def define_single_role_accessor(role, one_to_one)
223
- # puts "Defining #{basename}.#{role.name} to #{role.counterpart_object_type.basename} (#{one_to_one ? "assigning" : "populating"} #{role.counterpart.name})"
227
+ def define_one_to_one_accessor(role)
224
228
  define_single_role_getter(role)
225
229
 
226
- if (one_to_one)
227
- # This gets called to assign nil to the related role in the old correspondent:
228
- # value is included here so we can check that the correct value is being nullified, if necessary
229
- nullify_reference = lambda{|from, role_name, value| from.send("#{role_name}=".to_sym, nil) }
230
+ class_eval do
231
+ define_method role.setter do |value|
232
+ role_var = role.variable
230
233
 
231
- # This gets called to replace an old single value for a new one in the related role of a new correspondent
232
- assign_reference = lambda{|from, role_name, old_value, value| from.send("#{role_name}=".to_sym, value) }
234
+ # Get old value, and jump out early if it's unchanged:
235
+ old = instance_variable_get(role_var) rescue nil
236
+ return value if old.equal?(value) # Occurs when another instance having the same value is assigned
233
237
 
234
- define_single_role_setter(role, nullify_reference, assign_reference)
235
- else
236
- # This gets called to delete this object from the role value array in the old correspondent
237
- delete_reference = lambda{|from, role_name, value| from.send(role_name).update(value, nil) }
238
+ value = role.adapt(constellation, value) if value
239
+ return value if old.equal?(value) # Occurs when same value but not same instance is assigned
240
+
241
+ # REVISIT: A frozen-key solution could be used to allow changing identifying roles.
242
+ # If this object plays an identifying role in other objects, they also need re-indexing
243
+ # if role.is_identifying
244
+ # raise "#{self.class.basename}: illegal attempt to modify identifying role #{role.name}" if value != nil && old != nil
245
+ # end
238
246
 
239
- # This gets called to replace an old value by a new one in the related role value array of a new correspondent
240
- replace_reference = lambda{|from, role_name, old_value, value|
241
- from.send(role_name).update(old_value, value)
242
- }
247
+ instance_variable_set(role_var, value)
243
248
 
244
- define_single_role_setter(role, delete_reference, replace_reference)
249
+ # Remove self from the old counterpart:
250
+ old.send(role.counterpart.setter, nil) if old
251
+
252
+ # Assign self to the new counterpart
253
+ value.send(role.counterpart.setter, self) if value
254
+
255
+ value
256
+ end
245
257
  end
246
258
  end
247
259
 
248
- def define_single_role_setter(role, deassign_old, assign_new)
249
- class_eval do
250
- define_method "#{role.name}=" do |value|
251
- role_var = "@#{role.name}"
260
+ def define_one_to_many_accessor(role)
261
+ define_single_role_getter(role)
252
262
 
253
- # If role.counterpart_object_type isn't bound to a class yet, bind it.
254
- role.resolve_counterpart(self.class.vocabulary) unless role.counterpart_object_type.is_a?(Class)
263
+ class_eval do
264
+ define_method role.setter do |value|
265
+ role_var = role.variable
255
266
 
256
267
  # Get old value, and jump out early if it's unchanged:
257
268
  old = instance_variable_get(role_var) rescue nil
258
- return if old == value # Occurs during one_to_one assignment, for example
269
+ return value if old.equal?(value) # Occurs during one_to_one assignment, for example
259
270
 
260
271
  value = role.adapt(constellation, value) if value
261
- return if old == value # Occurs when same value is assigned
262
-
263
- # DEBUG: puts "assign #{self.class.basename}.#{role.name} <-> #{value.inspect}.#{role.counterpart.name}#{old ? " (was #{old.inspect})" : ""}"
272
+ return value if old.equal?(value) # Occurs when another instance having the same value is assigned
264
273
 
265
274
  # REVISIT: A frozen-key solution could be used to allow changing identifying roles.
275
+ # If this object plays an identifying role in other objects, they need re-indexing
266
276
  # The key would be frozen, allowing indices and counterparts to de-assign,
267
277
  # but delay re-assignment until defrosted.
268
278
  # That would also allow caching the identifying_role_values, a performance win.
269
279
 
270
280
  # This allows setting and clearing identifying roles, but not changing them.
271
- raise "#{self.class.basename}: illegal attempt to modify identifying role #{role.name}" if role.is_identifying && value != nil && old != nil
281
+ # if role.is_identifying
282
+ # raise "#{self.class.basename}: illegal attempt to modify identifying role #{role.name}" if value != nil && old != nil
283
+ # end
272
284
 
273
- # puts "Setting binary #{role_var} to #{value.verbalise}"
274
285
  instance_variable_set(role_var, value)
275
286
 
276
- # De-assign/remove "self" at the old other end too:
277
- deassign_old.call(old, role.counterpart.name, self) if old
287
+ # Remove "self" from the old counterpart:
288
+ old.send(getter = role.counterpart.getter).update(self, nil) if old
278
289
 
279
- # Assign/add "self" at the other end too:
280
- assign_new.call(value, role.counterpart.name, old, self) if value
290
+ # Add "self" into the counterpart
291
+ value.send(getter ||= role.counterpart.getter).update(old, self) if value
292
+
293
+ value
281
294
  end
282
295
  end
283
296
  end
284
297
 
285
- def define_array_role_accessor(role)
298
+ def define_many_to_one_accessor(role)
286
299
  class_eval do
287
- define_method "#{role.name}" do
288
- unless (r = instance_variable_get(role_var = "@#{role.name}") rescue nil)
300
+ define_method role.getter do
301
+ unless (r = instance_variable_get(role_var = role.variable) rescue nil)
289
302
  r = instance_variable_set(role_var, RoleValues.new)
290
303
  end
291
- # puts "fetching #{self.class.basename}.#{role.name} array, got #{r.class}, first is #{r[0] ? r[0].verbalise : "nil"}"
292
304
  r
293
305
  end
294
306
  end
@@ -319,12 +331,11 @@ module ActiveFacts
319
331
  # Role Name
320
332
  # else:
321
333
  # Leading Adjective
322
- # Role counterpart_object_type name (not role name)
334
+ # Role counterpart object_type name (not role name)
323
335
  # Trailing Adjective
324
- # "_as_<other_role_name>" if other_role_name != this role counterpart_object_type's name, and not other_player_this_player
336
+ # "_as_<other_role_name>" if other_role_name != this role's counterpart' object_type name, and not other_player_this_player
325
337
  def extract_binary_params(one_to_one, role_name, options)
326
338
  # Options:
327
- # other counterpart_object_type (Symbol or Class)
328
339
  # mandatory (:mandatory)
329
340
  # other end role name if any (Symbol),
330
341
  related = nil
@@ -332,8 +343,7 @@ module ActiveFacts
332
343
  related_role_name = nil
333
344
  role_player = self.basename.snakecase
334
345
 
335
- role_name = a.name.snakecase.to_sym if Class === role_name
336
- role_name = role_name.to_sym
346
+ role_name = (Class === role_name ? a.name.snakecase : role_name).to_sym
337
347
 
338
348
  # The related class might be forward-referenced, so handle a Symbol/String instead of a Class.
339
349
  related_name = options.delete(:class)
@@ -353,9 +363,10 @@ module ActiveFacts
353
363
 
354
364
  # resolve the Symbol to a Class now if possible:
355
365
  resolved = vocabulary.object_type(related) rescue nil
356
- #puts "#{related} resolves to #{resolved}"
357
366
  related = resolved if resolved
358
- # puts "related = #{related.inspect}"
367
+ if related.is_a?(Class)
368
+ raise "#{related} must be an object type in #{vocabulary.name}" unless related.respond_to?(:vocabulary) and related.vocabulary == self.vocabulary
369
+ end
359
370
 
360
371
  if options.delete(:mandatory) == true
361
372
  mandatory = true
@@ -385,7 +396,6 @@ module ActiveFacts
385
396
  (!related_role_name || related_role_name == role_player)
386
397
  other_role_method += "_as_#{role_name}"
387
398
  end
388
- #puts "On #{basename}: have related_role_name=#{related_role_name.inspect}, role_player=#{role_player}, role_name=#{role_name}, related_name=#{related_name.inspect} -> #{related_name}.#{other_role_method}"
389
399
 
390
400
  [ role_name,
391
401
  related,
@@ -402,8 +412,6 @@ module ActiveFacts
402
412
  vocabulary.__delay(object_type.to_s.camelcase, args, &block)
403
413
  when String # Arrange for this to happen later
404
414
  vocabulary.__delay(object_type, args, &block)
405
- else
406
- raise "Delayed binding not possible for #{object_type.class.name} #{object_type.inspect}"
407
415
  end
408
416
  end
409
417
  end