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.
data/TODO CHANGED
@@ -1,4 +1,20 @@
1
1
  Performance
2
+ Each object type (class) needs fast access to:
3
+ Its identifying roles
4
+ Its supertypes that have alternate identification
5
+ The roles it plays in identifying other object types
6
+ Each one-to-many role needs:
7
+ A method to derive a counterpart (RoleValues) key from a full key
8
+ Each class needs
9
+ An adapt() method to convert offered values to a full key
10
+ an assign_all method
11
+ that can perform "atomic" identity change
12
+ Constellation needs assert_instance that for a given class:
13
+ adapts all keys from identifying values
14
+ checks either non-existence or uniqueness and type of the identified object for all such keys
15
+ instantiates the object if previously non-existent
16
+ (instantiates role subtypes by mixing in if this instance did not exist but the class is mixed in another extant object)
17
+ assigns non-identifying values
2
18
  Pre-define ObjectType accessor methods on constellation, rather than using method_missing
3
19
 
4
20
  Role objects:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.3
1
+ 0.9.4
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "activefacts-api"
8
- s.version = "0.9.3"
8
+ s.version = "0.9.4"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Clifford Heath"]
12
- s.date = "2012-11-01"
12
+ s.date = "2013-01-14"
13
13
  s.description = "\nThe ActiveFacts API is a Ruby DSL for managing constellations of elementary facts.\nEach fact is either existential (a value or an entity), characteristic (boolean) or\nbinary relational (A rel B). Relational facts are consistently co-referenced, so you\ncan traverse them efficiently in any direction. Each constellation maintains constraints\nover the fact population.\n"
14
14
  s.email = "clifford.heath@gmail.com"
15
15
  s.extra_rdoc_files = [
@@ -29,6 +29,7 @@ Gem::Specification.new do |s|
29
29
  "activefacts-api.gemspec",
30
30
  "lib/activefacts/api.rb",
31
31
  "lib/activefacts/api/constellation.rb",
32
+ "lib/activefacts/api/date.rb",
32
33
  "lib/activefacts/api/entity.rb",
33
34
  "lib/activefacts/api/exceptions.rb",
34
35
  "lib/activefacts/api/guid.rb",
@@ -52,6 +53,7 @@ Gem::Specification.new do |s|
52
53
  "spec/identification_scheme/identification_spec.rb",
53
54
  "spec/identification_scheme/identity_change_spec.rb",
54
55
  "spec/identification_scheme/identity_supertype_change_spec.rb",
56
+ "spec/metadata_spec.rb",
55
57
  "spec/object_type/entity_type/entity_type_spec.rb",
56
58
  "spec/object_type/entity_type/multipart_identification_spec.rb",
57
59
  "spec/object_type/value_type/autocounter_spec.rb",
@@ -33,25 +33,83 @@ module ActiveFacts
33
33
  #
34
34
  class Constellation
35
35
  attr_reader :vocabulary
36
- # All instances are indexed in this hash, keyed by the class object.
36
+
37
+ def valid_object_type klass
38
+ klass.is_a?(Class) and klass.modspace == @vocabulary and klass.respond_to?(:assert_instance)
39
+ end
40
+
41
+ # "instances" is an index (keyed by the Class object) of indexes to instances.
37
42
  # Each instance is indexed for every supertype it has (including multiply-inherited ones).
38
- # It's a bad idea to try to modify these indexes!
39
- attr_reader :instances # Can say c.instances[MyClass].each{|k, v| ... }
40
- # Can also say c.MyClass.each{|k, v| ... }
43
+ # The method_missing definition supports the syntax: c.MyClass.each{|k, v| ... }
44
+ def instances
45
+ @instances ||= Hash.new do |h,k|
46
+ unless valid_object_type k
47
+ raise "A constellation over #{@vocabulary.name} can only index instances of classes in that vocabulary, not #{k.inspect}"
48
+ end
49
+ h[k] = InstanceIndex.new(self, k)
50
+ end
51
+ end
52
+
53
+ # Candidates is an array of object instances that do not already exist
54
+ # in the constellation but will be added if an assertion succeeds.
55
+ # After the assertion is found to be acceptable, these objects are indexed
56
+ # in the constellation and in the counterparts of their identifying roles,
57
+ # and the candidates array is nullified.
58
+ def with_candidates &b
59
+ # Multiple assignment reduces (s)teps while debugging
60
+ outermost, @candidates, @on_admission = @candidates.nil?, (@candidates || []), (@on_admission || [])
61
+ begin
62
+ b.call
63
+ rescue Exception
64
+ # Do not accept any of these candidates, there was a problem:
65
+ @candidates = [] if outermost
66
+ raise
67
+ ensure
68
+ if outermost
69
+ while @candidates
70
+ # Index the accepted instances in the constellation:
71
+ candidates = @candidates
72
+ on_admission = @on_admission
73
+ @candidates = nil
74
+ @on_admission = nil
75
+ candidates.each do |instance|
76
+ instance.class.index_instance(self, instance)
77
+ end
78
+ on_admission.each do |b|
79
+ b.call
80
+ end
81
+ # REVISIT: Admission should not create new candidates, but might start a fresh list
82
+ # debugger if @candidates and @candidates.length > 0
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ def when_admitted &b
89
+ if @candidates.nil?
90
+ b.call self
91
+ else
92
+ @on_admission << b
93
+ end
94
+ end
95
+
96
+ def candidate instance
97
+ @candidates << instance unless @candidates[-1] == instance
98
+ end
99
+
100
+ def has_candidate klass, key
101
+ @candidates && @candidates.detect{|c| c.is_a?(klass) && c.identifying_role_values == key }
102
+ end
41
103
 
42
104
  # Create a new empty Constellation over the given Vocabulary
43
105
  def initialize(vocabulary)
44
106
  @vocabulary = vocabulary
45
- @instances = Hash.new do |h,k|
46
- unless k.is_a?(Class) and k.modspace == vocabulary
47
- raise "A constellation over #{@vocabulary.name} can only index instances of classes in that vocabulary, not #{k.inspect}"
48
- end
49
- h[k] = InstanceIndex.new(self, k)
50
- end
51
107
  end
52
108
 
53
- def inspect #:nodoc:
54
- "Constellation:#{object_id}"
109
+ def assert(klass, *args)
110
+ with_candidates do
111
+ klass.assert_instance self, args
112
+ end
55
113
  end
56
114
 
57
115
  # Evaluate assertions against the population of this Constellation
@@ -68,6 +126,47 @@ module ActiveFacts
68
126
  self
69
127
  end
70
128
 
129
+ # This method removes the given instance from this constellation's indexes
130
+ # It must be called before the identifying roles get deleted or nullified.
131
+ def deindex_instance(instance) #:nodoc:
132
+ ([instance.class]+instance.class.supertypes_transitive).each do |klass|
133
+ instances[klass].delete(instance.identifying_role_values(klass))
134
+ end
135
+ # REVISIT: Need to nullify all the roles this object plays.
136
+ # If mandatory on the counterpart side, this may/must propagate the delete (without mutual recursion!)
137
+ end
138
+
139
+ def define_class_accessor m, klass
140
+ (class << self; self; end).
141
+ send(:define_method, m) do |*args|
142
+ if args.size == 0
143
+ # Return the collection of all instances of this class in the constellation:
144
+ instances[klass]
145
+ else
146
+ # Assert a new ground fact (object_type instance) of the specified class, identified by args:
147
+ assert(klass, *args)
148
+ end
149
+ end
150
+ end
151
+
152
+ # If a missing method is the name of a class in the vocabulary module for this constellation,
153
+ # then we want to access the collection of instances of that class, and perhaps assert new ones.
154
+ # With no parameters, return the collection of all instances of that object_type.
155
+ # With parameters, assert an instance of the object_type identified by the values passed as args.
156
+ def method_missing(m, *args, &b)
157
+ klass = @vocabulary.const_get(m)
158
+ if valid_object_type klass
159
+ define_class_accessor m, klass
160
+ send(m, *args, &b)
161
+ else
162
+ super
163
+ end
164
+ end
165
+
166
+ def inspect #:nodoc:
167
+ "Constellation:#{object_id}"
168
+ end
169
+
71
170
  # Constellations verbalise all members of all classes in alphabetical order, showing
72
171
  # non-identifying role values as well
73
172
  def verbalise
@@ -88,8 +187,13 @@ module ActiveFacts
88
187
  if (single_roles.size > 0)
89
188
  role_values =
90
189
  single_roles.map{|role|
91
- [ role_name = role.to_s.camelcase,
92
- value = instance.send(role)]
190
+ value =
191
+ begin
192
+ value = instance.send(role)
193
+ rescue NoMethodError
194
+ instance.class.roles(role) # This role has not yet been realised
195
+ end
196
+ [ role_name = role.to_s.camelcase, value ]
93
197
  }.select{|role_name, value|
94
198
  value
95
199
  }.map{|role_name, value|
@@ -102,42 +206,6 @@ module ActiveFacts
102
206
  end.compact*"\n"
103
207
  end
104
208
 
105
- # This method removes the given instance from this constellation's indexes
106
- # It must be called before the identifying roles get deleted or nullified.
107
- def __retract(instance) #:nodoc:
108
- # REVISIT: Need to search, as key values are gone already. Is there a faster way?
109
- ([instance.class]+instance.class.supertypes_transitive).each do |klass|
110
- @instances[klass].delete_if{|k,v| v == instance }
111
- end
112
- # REVISIT: Need to nullify all the roles this object plays.
113
- # If mandatory on the counterpart side, this may/must propagate the delete (without mutual recursion!)
114
- end
115
-
116
- # If a missing method is the name of a class in the vocabulary module for this constellation,
117
- # then we want to access the collection of instances of that class, and perhaps assert new ones.
118
- # With no parameters, return the collection of all instances of that object_type.
119
- # With parameters, assert an instance of the object_type identified by the values passed as args.
120
- def method_missing(m, *args, &b)
121
- klass = @vocabulary.const_get(m)
122
- if klass and klass.is_a?(Class) and klass.respond_to?(:assert_instance)
123
- (class << self; self; end).
124
- send(:define_method, sym = m.to_sym) do |*args|
125
- instance_index = @instances[klass]
126
- if args.size == 0
127
- # Return the collection of all instances of this class in the constellation:
128
- instance_index
129
- else
130
- # Assert a new ground fact (object_type instance) of the specified class, identified by args:
131
- instance_index.assert(*args)
132
- end
133
- end
134
-
135
- # This is the last time it'll be missing, so call it.
136
- send(sym, *args, &b)
137
- else
138
- super
139
- end
140
- end
141
209
  end
142
210
  end
143
211
  end
@@ -0,0 +1,98 @@
1
+ #
2
+ # ActiveFacts Runtime API
3
+ # Date hacks to handle immediate types.
4
+ #
5
+ # Copyright (c) 2013 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ # Date and DateTime don't have a sensible new() method, so we monkey-patch one here.
8
+ #
9
+ require 'date'
10
+ require 'time'
11
+
12
+ # A Date can be constructed from any Date or DateTime subclass, or parsed from a String
13
+ class ::Date
14
+ def self.new_instance constellation, *a, &b
15
+ if a[0].is_a?(String)
16
+ d = parse(*a)
17
+ elsif (a.size == 1)
18
+ case a[0]
19
+ when DateTime
20
+ d = civil(a[0].year, a[0].month, a[0].day, a[0].start)
21
+ when Date
22
+ d = civil(a[0].year, a[0].month, a[0].day, a[0].start)
23
+ when NilClass
24
+ d = civil()
25
+ else
26
+ d = civil(*a, &b)
27
+ end
28
+ else
29
+ d = civil(*a, &b)
30
+ end
31
+ d.send(:instance_variable_set, :@constellation, constellation)
32
+ d
33
+ end
34
+ end
35
+
36
+ # A DateTime can be constructed from any Date or DateTime subclass, or parsed from a String
37
+ class ::DateTime
38
+
39
+ def self.new_instance constellation, *a, &b
40
+ if a[0].is_a?(String)
41
+ dt = parse(*a)
42
+ elsif (a.size == 1)
43
+ case a[0]
44
+ when DateTime
45
+ dt = civil(a[0].year, a[0].month, a[0].day, a[0].hour, a[0].min, a[0].sec, a[0].start)
46
+ when Date
47
+ dt = civil(a[0].year, a[0].month, a[0].day, 0, 0, 0, a[0].start)
48
+ when NilClass
49
+ dt = civil()
50
+ else
51
+ dt = civil(*a, &b)
52
+ end
53
+ else
54
+ dt = civil(*a, &b)
55
+ end
56
+ dt.send(:instance_variable_set, :@constellation, constellation)
57
+ dt
58
+ end
59
+
60
+ end
61
+
62
+ class ::Time
63
+ def identifying_role_values; self; end
64
+
65
+ def self.new_instance constellation, *a
66
+ t =
67
+ if a[0].is_a?(Time)
68
+ at(a[0])
69
+ else
70
+ begin
71
+ local(*a)
72
+ end
73
+ end
74
+
75
+ =begin
76
+ if a[0].is_a?(String)
77
+ parse(*a)
78
+ elsif (a.size == 1)
79
+ case a[0]
80
+ when DateTime
81
+ a[0].clone
82
+ when Date
83
+ civil(a[0].year, a[0].month, a[0].day, 0, 0, 0, a[0].start)
84
+ when NilClass
85
+ civil()
86
+ else
87
+ civil(*a, &b)
88
+ end
89
+ else
90
+ civil(*a, &b)
91
+ end
92
+ =end
93
+
94
+ t.send(:instance_variable_set, :@constellation, constellation)
95
+ t
96
+ end
97
+
98
+ end
@@ -11,69 +11,61 @@ module ActiveFacts
11
11
  module Entity
12
12
  include Instance
13
13
 
14
- # Assign the identifying roles to initialise a new Entity instance.
15
- # The role values are asserted in the constellation first, so you
16
- # can pass bare values (array, string, integer, etc) for any role
17
- # whose instances can be constructed using those values.
14
+ private
15
+ # Initialise a new Entity instance.
18
16
  #
19
- # A value must be provided for every identifying role, but if the
20
- # last argument is a hash, they may come from there.
17
+ # arg_hash contains full-normalised and valid keys for the counterpart
18
+ # role values.
21
19
  #
22
- # If a supertype (including a secondary supertype) has a different
23
- # identifier, the identifying roles must be provided in the hash.
20
+ # This instance and its supertypes might have distinct identifiers,
21
+ # and none of the identifiers may already exist in the constellation.
24
22
  #
25
- # Any additional (non-identifying) roles in the hash are ignored
26
- def initialize(*args)
27
- klass = self.class
28
- while klass.identification_inherited_from
29
- klass = klass.superclass
30
- end
31
-
32
- if args[-1].respond_to?(:has_key?) && args[-1].has_key?(:constellation)
33
- @constellation = args.pop[:constellation]
34
- end
35
- hash = args[-1].is_a?(Hash) ? args.pop.clone : nil
36
-
37
- # Pass just the hash, if there is one, else no arguments:
38
- super(*(hash ? [hash] : []))
39
-
40
- # Pick any missing identifying roles out of the hash if possible:
41
- irns = klass.identifying_role_names
42
- while hash && args.size < irns.size
43
- value = hash[role = irns[args.size]]
44
- hash.delete(role)
45
- args.push value
46
- end
47
-
48
- # If one arg is expected but more are passed, they might be the
49
- # args for the object that plays a single identifying role:
50
- args = [args] if klass.identifying_role_names.size == 1 && args.size > 1
51
-
52
- # This occur when there are too many args passed, or too few
53
- # and no hash. Otherwise the missing ones will be nil.
54
- raise "Wrong number of parameters to #{klass}.new, " +
55
- "expect (#{klass.identifying_role_names*","}) " +
56
- "got (#{args.map{|a| a.to_s.inspect}*", "})" if args.size != klass.identifying_role_names.size
57
-
58
- # Assign the identifying roles in order. Any other roles will be assigned by our caller
59
- klass.identifying_role_names.zip(args).each do |role_name, value|
60
- role = self.class.roles(role_name)
61
- begin
62
- send(role.setter, value)
63
- rescue NoMethodError => e
64
- raise settable_roles_exception(e, role_name)
65
- end
23
+ # Pick out the identifying roles and assert the counterpart instances
24
+ # to assign as the new object's role values.
25
+ #
26
+ # The identifying roles of secondary supertypes must also be assigned
27
+ # here.
28
+ def initialize(arg_hash)
29
+ raise "REVISIT: Unexpected parameters in call to #{self}.new" unless arg_hash.is_a?(Hash)
30
+
31
+ super(arg_hash)
32
+
33
+ unless (klass = self.class).identification_inherited_from
34
+ irns = klass.identifying_role_names
35
+ irns.each do |role_name|
36
+ role = klass.roles(role_name)
37
+ key = arg_hash.delete(role_name)
38
+ value =
39
+ if key == nil
40
+ nil
41
+ elsif role.is_unary
42
+ (key && true) # Preserve nil and false
43
+ else
44
+ role.counterpart.object_type.assert_instance(constellation, Array(key))
45
+ end
46
+
47
+ begin
48
+ send(role.setter, value)
49
+ # rescue NoMethodError => e
50
+ # raise settable_roles_exception(e, role_name)
51
+ end
52
+ # instance_variable_set(role.setter, value)
53
+ end
66
54
  end
67
55
  end
68
56
 
57
+ =begin
58
+ # I forget how it was possible to reproduce this exception,
59
+ # so I can't get code coverage over it. It might not be still possible,
60
+ # but I can't be sure so I'll leave the code here for now.
69
61
  def settable_roles_exception e, role_name
70
- n = e.class.new(
71
- "#{self.class} has no setter for #{role_name}.\n" +
62
+ n = NoMethodError.new(
63
+ "You cannot assert a #{self.class} until you define #{role_name}.\n" +
72
64
  "Settable roles are #{settable_roles*', '}.\n" +
73
65
  (if self.class.vocabulary.delayed.empty?
74
66
  ''
75
67
  else
76
- "This could be because the following expected object types are still not defined: #{self.class.vocabulary.delayed.keys.sort*', '}\n"
68
+ "Please define these object types: #{self.class.vocabulary.delayed.keys.sort*', '}\n"
77
69
  end
78
70
  )
79
71
  )
@@ -92,7 +84,9 @@ module ActiveFacts
92
84
  end.
93
85
  flatten
94
86
  end
87
+ =end
95
88
 
89
+ public
96
90
  def inspect #:nodoc:
97
91
  inc = constellation ? " in #{constellation.inspect}" : ""
98
92
  # REVISIT: Where there are one-to-one roles, this cycles
@@ -133,10 +127,11 @@ module ActiveFacts
133
127
  "#{role_name || self.class.basename}(#{ irnv*', ' })"
134
128
  end
135
129
 
136
- # Return the array of the values of this entity instance's identifying roles
137
- def identifying_role_values
138
- self.class.identifying_role_names.map do |role_name|
139
- send(role_name).identifying_role_values
130
+ # Return the array of the values of this instance's identifying roles
131
+ def identifying_role_values(klass = self.class)
132
+ klass.identifying_role_names.map do |role_name|
133
+ value = send(role_name)
134
+ value.identifying_role_values
140
135
  end
141
136
  end
142
137
 
@@ -193,160 +188,150 @@ module ActiveFacts
193
188
  end
194
189
  end
195
190
 
196
- # Convert the passed arguments into an array of raw values (or arrays of values, transitively)
197
- # that identify an instance of this Entity type:
198
- def identifying_role_values(*args)
199
- irns = identifying_role_names
200
-
201
- # If the single arg is an instance of the correct class or a subclass,
202
- # use the instance's identifying_role_values
203
- has_hash = args[-1].is_a?(Hash)
204
- if (args.size == 1+(has_hash ? 1 : 0) and (arg = args[0]).is_a?(self))
205
- # With a secondary supertype or a subtype having separate identification,
206
- # we would get the wrong identifier from arg.identifying_role_values:
207
- return irns.map do |role_name|
208
- # Use the identifier for the class expected, not the actual:
209
- value = arg.send(role_name)
210
- value && arg.class.roles(role_name).counterpart_object_type.identifying_role_values(value)
211
- end
212
- end
213
-
214
- args, arg_hash = ActiveFacts::extract_hash_args(irns, args)
215
-
216
- if args.size > irns.size
217
- raise "#{basename} expects only (#{irns*', '}) for its identifier, but you provided the extra values #{args[irns.size..-1].inspect}"
218
- end
219
-
220
- role_args = irns.map{|role_sym| roles(role_sym)}.zip(args)
221
- role_args.map do |role, arg|
222
- next !!arg unless role.counterpart # Unary
223
- if arg.is_a?(role.counterpart.object_type) # includes secondary supertypes
224
- # With a secondary supertype or a type having separate identification,
225
- # we would get the wrong identifier from arg.identifying_role_values:
226
- next role.counterpart_object_type.identifying_role_values(arg)
227
- end
228
- if arg == nil # But not false
229
- if role.mandatory
230
- raise MissingMandatoryRoleValueException.new(self, role)
231
- end
232
- else
233
- role.counterpart_object_type.identifying_role_values(*arg)
234
- end
235
- end
236
- end
237
-
238
- # REVISIT: This method should verify that all identifying roles (including
239
- # those required to identify any superclass) are present (if mandatory)
240
- # and are unique... BEFORE it creates any new object(s)
241
- # This is a hard problem because it's recursive.
242
- def assert_instance(constellation, args) #:nodoc:
243
- # Build the key for this instance from the args
244
- # The key of an instance is the value or array of keys of the identifying values.
245
- # The key values aren't necessarily present in the constellation, even after this.
246
- key = identifying_role_values(*args)
247
-
248
- # Find and return an existing instance matching this key
249
- instances = constellation.instances[self] # All instances of this class in this constellation
250
- instance = instances[key]
251
- @created_instances ||= []
252
- if instance
253
- # raise "Additional role values are ignored when asserting an existing instance" if args[-1].is_a? Hash and !args[-1].empty?
254
- assign_additional_roles(instance, args[-1]) if args[-1].is_a? Hash and !args[-1].empty?
255
- return instance, key # A matching instance of this class
256
- end
257
-
258
- # Now construct each of this object's identifying roles
191
+ def check_supertype_identifiers_match instance, arg_hash
192
+ supertypes_transitive.each do |supertype|
193
+ supertype.identifying_role_names.each do |role_name|
194
+ next unless arg_hash.include?(role_name) # No contradiction here
195
+ new_value = arg_hash[role_name]
196
+ existing_value = instance.send(role_name.to_sym)
197
+
198
+ # Quick check for an exact match:
199
+ next if existing_value == new_value or existing_value.identifying_role_values == new_value
200
+
201
+ # Coerce the new value to identifying values for the counterpart role's type:
202
+ role = supertype.roles(role_name)
203
+ new_key = role.counterpart.object_type.identifying_role_values(instance.constellation, [new_value])
204
+ # REVISIT: Check that the next line actually gets hit, otherwise strip it out
205
+ next if existing_value == new_key # This can happen when the counterpart is a value type
206
+
207
+ existing_key = existing_value.identifying_role_values
208
+ next if existing_key.identifying_role_values == new_key
209
+ raise TypeConflictException.new(basename, supertype, new_key, existing_key)
210
+ end
211
+ end
212
+ end
213
+
214
+ # all its candidate keys must match those from the arg_hash.
215
+ def check_no_supertype_instance_exists constellation, arg_hash
216
+ supertypes_transitive.each do |supertype|
217
+ key = supertype.identifying_role_values(constellation, [arg_hash])
218
+ if constellation.instances[supertype][key]
219
+ raise TypeMigrationException.new(basename, supertype, key)
220
+ end
221
+ end
222
+ end
223
+
224
+ # This method receives an array (possibly including a trailing arguments hash)
225
+ # from which the values of identifying roles must be coerced. Note that when a
226
+ # value which is not the corrent class is received, we recurse to ask that class
227
+ # to coerce what we *do* have.
228
+ # The return value is an array of (and arrays of) raw values, not object instances.
229
+ #
230
+ # No new instances may be asserted, nor may any roles of objects in the constellation be changed
231
+ def identifying_role_values(constellation, args)
259
232
  irns = identifying_role_names
260
233
 
261
- has_hash = args[-1].is_a?(Hash)
262
- if args.size == 1+(has_hash ? 1 : 0) and args[0].is_a?(self)
263
- # We received a single argument of a compatible type
264
- # With a secondary supertype or a type having separate identification,
265
- # we would get the wrong identifier from arg.identifying_role_values:
266
- key =
267
- values = identifying_role_values(args[0])
268
- values = values + [arg_hash = args.pop] if has_hash
269
- else
270
- args, arg_hash = ActiveFacts::extract_hash_args(irns, args)
271
- roles_and_values = irns.map{|role_sym| roles(role_sym)}.zip(args)
272
- key = [] # Gather the actual key (AutoCounters are special)
273
- values = roles_and_values.map do |role, arg|
274
- if role.unary?
275
- # REVISIT: This could be absorbed into a special counterpart.object_type.assert_instance
276
- value = role_key = arg ? true : arg # Preserve false and nil
277
- elsif !arg
278
- value = role_key = nil
279
- else
280
- =begin
281
- # REVISIT; These next few lines are bogus; they generate TypeError exceptions which are caught and ignored
282
- # A new approach will follow shortly which won't @create_instances that won't be needed
283
- if role.counterpart.object_type.is_entity_type
284
- add = !constellation.send(role.counterpart.object_type.basename.to_sym).include?([arg])
285
- else
286
- add = !constellation.send(role.counterpart.object_type.basename.to_sym).include?(arg)
287
- end
288
- =end
289
- value, role_key = role.counterpart.object_type.assert_instance(constellation, Array(arg))
290
- # @created_instances << [role.counterpart, value] if add
291
- end
292
- key << role_key
293
- value
294
- end
295
- values << arg_hash if arg_hash and !arg_hash.empty?
296
- end
297
-
298
- #trace :assert, "Constructing new #{self} with #{values.inspect}" do
299
- values << { :constellation => constellation }
300
- instance = new(*values)
301
- #end
302
-
303
- assign_additional_roles(instance, arg_hash)
304
-
305
- return *index_instance(instance, key, irns)
306
-
307
- rescue DuplicateIdentifyingValueException
308
- @created_instances.each do |role, v|
309
- if !v.respond_to?(:retract)
310
- v = constellation.send(role.object_type.basename.to_sym)[[v]]
311
- end
312
- v.retract if v
313
- end
314
- @created_instances = []
315
- raise
316
- end
317
-
318
- def assign_additional_roles(instance, arg_hash)
319
- # Now assign any extra args in the hash which weren't identifiers (extra identifiers will be assigned again)
320
- (arg_hash ? arg_hash.entries : []).each do |role_name, value|
321
- role = roles(role_name)
322
-
323
- if !instance.instance_index_counterpart(role).include?(value)
324
- @created_instances << [role, value]
325
- end
326
- instance.send(role.setter, value)
327
- end
328
- end
329
-
330
- def index_instance(instance, key = nil, key_roles = nil) #:nodoc:
331
- # Derive a new key if we didn't receive one or if the roles are different:
332
- unless key && key_roles && key_roles == identifying_role_names
333
- key = (key_roles = identifying_role_names).map do |role_name|
334
- instance.send role_name
335
- end
336
- raise "You must pass values for #{key_roles.inspect} to identify a #{self.name}" if key.compact == []
337
- end
338
-
339
- # Index the instance for this class in the constellation
340
- instances = instance.constellation.instances[self]
341
- instances[key] = instance
234
+ # Normalise positional arguments into an arguments hash (this changes the passed parameter)
235
+ arg_hash = args[-1].is_a?(Hash) ? args.pop : {}
236
+
237
+ # If the first parameter is an object of type self, its
238
+ # identifying roles provide any values missing from the array/hash.
239
+ if args[0].is_a?(self)
240
+ proto = args.shift
241
+ end
242
+
243
+ # Following arguments provide identifying values in sequence; put them into the hash:
244
+ irns.each do |role_name|
245
+ break if args.size == 0
246
+ arg_hash[role_name] = args.shift
247
+ end
248
+
249
+ # Complain if we have left-over arguments
250
+ if args.size > 0
251
+ raise "#{basename} expects only (#{irns*', '}) for its identifier, but you provided additional values #{args.inspect}"
252
+ end
253
+
254
+ # The arg_hash will be used to construct a new instance, if necessary
255
+ args.push(arg_hash)
256
+
257
+ irns.map do |role_name|
258
+ roles(role_name)
259
+ end.map do |role|
260
+ if arg_hash.include?(n = role.name) # Do it this way to avoid problems where nil or false is provided
261
+ value = arg_hash[n]
262
+ next (value && true) if (role.is_unary)
263
+ if value
264
+ klass = role.counterpart.object_type
265
+ value = klass.identifying_role_values(constellation, Array(value))
266
+ end
267
+ elsif proto
268
+ value = proto.send(n)
269
+ arg_hash[n] = value.identifying_role_values # Save the value for making a new instance
270
+ next value if (role.is_unary)
271
+ else
272
+ value = nil
273
+ end
274
+
275
+ raise MissingMandatoryRoleValueException.new(self, role) if value.nil? && role.mandatory
276
+
277
+ value
278
+ end
279
+ end
280
+
281
+ def assert_instance(constellation, args)
282
+ key = identifying_role_values(constellation, args)
283
+
284
+ # The args is now normalized to an array containing a single Hash element
285
+ arg_hash = args[-1]
286
+
287
+ # Find or make an instance of the class:
288
+ instance_index = constellation.instances[self] # All instances of this class in this constellation
289
+ instance = constellation.has_candidate(self, key) || instance_index[key]
290
+ if (instance)
291
+ # Check that all assertions about supertype keys are non-contradictory
292
+ check_supertype_identifiers_match(instance, arg_hash)
293
+ else
294
+ # Check that no instance of any supertype matches the keys given
295
+ check_no_supertype_instance_exists(constellation, arg_hash)
296
+
297
+ instance = new_instance(constellation, arg_hash)
298
+ constellation.candidate(instance)
299
+ end
300
+
301
+ # Assign any extra roles that may have been passed.
302
+ # An exception here leaves the object indexed,
303
+ # but without the offending role (re-)assigned.
304
+ arg_hash.each do |k, v|
305
+ role = instance.class.roles(k)
306
+ unless role.is_identifying && role.object_type == self
307
+ value =
308
+ if v == nil
309
+ nil
310
+ elsif role.is_unary
311
+ (v && true) # Preserve nil and false
312
+ else
313
+ role.counterpart.object_type.assert_instance(constellation, Array(v))
314
+ end
315
+ instance.send(:"#{k}=", value)
316
+ end
317
+ end
318
+
319
+ instance
320
+ end
321
+
322
+ def index_instance(constellation, instance) #:nodoc:
323
+ # Index the instance in the constellation's InstanceIndex for this class:
324
+ instance_index = constellation.instances[self]
325
+ key = instance.identifying_role_values(self)
326
+ instance_index[key] = instance
342
327
 
343
328
  # Index the instance for each supertype:
344
- supertypes.each do |supertype|
345
- supertype.index_instance(instance, key, key_roles)
346
- end
329
+ supertypes.each do |supertype|
330
+ supertype.index_instance(constellation, instance)
331
+ end
347
332
 
348
- return instance, key
349
- end
333
+ instance
334
+ end
350
335
 
351
336
  # A object_type that isn't a ValueType must have an identification scheme,
352
337
  # which is a list of roles it plays. The identification scheme may be
@@ -382,9 +367,16 @@ module ActiveFacts
382
367
  end
383
368
  end
384
369
 
385
- def Entity.included other #:nodoc:
370
+ def self.included other #:nodoc:
386
371
  other.send :extend, ClassMethods
387
372
 
373
+ def other.new_instance constellation, *args
374
+ instance = allocate
375
+ instance.instance_variable_set("@constellation", constellation)
376
+ instance.send(:initialize, *args)
377
+ instance
378
+ end
379
+
388
380
  # Register ourselves with the parent module, which has become a Vocabulary:
389
381
  vocabulary = other.modspace
390
382
  unless vocabulary.respond_to? :object_type # Extend module with Vocabulary if necessary