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