activefacts-api 0.8.9 → 0.8.10

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/Rakefile CHANGED
@@ -7,7 +7,7 @@ Jeweler::Tasks.new do |gem|
7
7
  gem.name = "activefacts-api"
8
8
  gem.homepage = "http://github.com/cjheath/activefacts-api"
9
9
  gem.license = "MIT"
10
- gem.summary = "A semantic modeling and query language (CQL) and application runtime (the Constellation API)"
10
+ gem.summary = "A fact-based data model DSL and API"
11
11
  gem.description = %q{
12
12
  The ActiveFacts API is a Ruby DSL for managing constellations of elementary facts.
13
13
  Each fact is either existential (a value or an entity), characteristic (boolean) or
@@ -23,6 +23,7 @@ over the fact population.
23
23
  gem.add_development_dependency "bundler", "~> 1.0.0"
24
24
  gem.add_development_dependency "jeweler", "~> 1.5.2"
25
25
  # gem.add_development_dependency "rcov", ">= 0"
26
+ gem.add_development_dependency "rdoc", ">= 2.4.2"
26
27
  end
27
28
  Jeweler::RubygemsDotOrgTasks.new
28
29
 
@@ -34,12 +35,13 @@ end
34
35
 
35
36
  RSpec::Core::RakeTask.new(:rcov) do |spec|
36
37
  spec.pattern = 'spec/**/*_spec.rb'
38
+ spec.rcov_opts = [ '--exclude', 'spec', '--exclude', 'lib/activefacts/tracer.rb' ]
37
39
  spec.rcov = true
38
40
  end
39
41
 
40
42
  task :default => :spec
41
43
 
42
- require 'rake/rdoctask'
44
+ require 'rdoc/task'
43
45
  Rake::RDocTask.new do |rdoc|
44
46
  version = File.exist?('VERSION') ? File.read('VERSION') : ""
45
47
 
data/TODO ADDED
@@ -0,0 +1,29 @@
1
+ Performance
2
+ Pre-define ObjectType accessor methods on constellation, rather than using method_missing
3
+
4
+ Role objects:
5
+ TEST: Access through class-level accessors
6
+
7
+ Reindexing on identifier change
8
+ De-index on change, arrange for re-index on completion
9
+ Re-index all objects this identifies (keep track of roles-as-identifier)
10
+ Save "key" value
11
+ Nested block to accumulate changed identifiers and re-index at end?
12
+
13
+ Switch to rbtree from Hash
14
+ For InstanceIndex
15
+ For RoleValues
16
+ Index into RoleValues by key residual, not full key (worth doing?)
17
+
18
+ Testing
19
+ Complete pending tests
20
+ Add heckle coverage
21
+
22
+ Constraints
23
+ Ensure role methods arguments are flexible for plugins
24
+ Make mandatory work (propagate retraction? or just optionally?)
25
+
26
+ Functionality
27
+ Finish Constellation.assert
28
+ Replace inspect by verbalise
29
+ Query API and execution
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.9
1
+ 0.8.10
@@ -32,13 +32,13 @@
32
32
 
33
33
  require 'activefacts/api/support' # General support code and core patches
34
34
  require 'activefacts/api/vocabulary' # A Ruby module may become a Vocabulary
35
- require 'activefacts/api/role_proxy' # Experimental proxy for has_one/one_to_one role accessors
36
35
  require 'activefacts/api/instance_index' # The index used by a constellation to record every instance
37
36
  require 'activefacts/api/constellation' # A Constellation is a query result or fact population
38
- require 'activefacts/api/object_type' # A Ruby class may become a ObjectType in a Vocabulary
37
+ require 'activefacts/api/object_type' # A Ruby class may become a ObjectType in a Vocabulary
39
38
  require 'activefacts/api/role' # A ObjectType has a collection of Roles
40
39
  require 'activefacts/api/role_values' # The container used for sets of role players in many_one's
41
40
  require 'activefacts/api/instance' # An Instance is an instance of a ObjectType class
42
41
  require 'activefacts/api/value' # A Value is an Instance of a value class (String, Numeric, etc)
43
42
  require 'activefacts/api/entity' # An Entity class is an Instance not of a value class
44
43
  require 'activefacts/api/standard_types' # Value classes are augmented so their subclasses may become Value Types
44
+ require 'activefacts/tracer'
@@ -42,7 +42,7 @@ module ActiveFacts
42
42
  @vocabulary = vocabulary
43
43
  @instances = Hash.new do |h,k|
44
44
  raise "A constellation over #{@vocabulary.name} can only index instances of object_types in that vocabulary, not #{k.inspect}" unless k.is_a?(Class) and k.modspace == vocabulary
45
- h[k] = InstanceIndex.new
45
+ h[k] = InstanceIndex.new(self, k)
46
46
  end
47
47
  end
48
48
 
@@ -51,9 +51,9 @@ module ActiveFacts
51
51
  end
52
52
 
53
53
  # Evaluate assertions against the population of this Constellation
54
- def populate *args, &block
55
- # REVISIT: Use args for something? Like options to enable/disable validation?
54
+ def populate &block
56
55
  instance_eval(&block)
56
+ self
57
57
  end
58
58
 
59
59
  # Delete instances from the constellation, nullifying (or cascading) the roles each plays
@@ -61,13 +61,35 @@ module ActiveFacts
61
61
  Array(instances).each do |i|
62
62
  i.retract
63
63
  end
64
+ self
64
65
  end
65
66
 
67
+ =begin
68
+ def assert *args
69
+ case
70
+ when args.size >= 1
71
+ args.each do |arg|
72
+ assert arg
73
+ end
74
+ when args[0].is_a?(Hash)
75
+ args[0].each do |key, value|
76
+ klass_name = key.is_a?(Symbol) ? key.to_s.camelcase : key.to_s
77
+ klass = vocabulary.const_get(klass_name)
78
+ send(klass_name).assert(*Array(value))
79
+ end
80
+ else
81
+ args.each do |arg|
82
+ assert(arg)
83
+ end
84
+ end
85
+ end
86
+ =end
87
+
66
88
  # Constellations verbalise all members of all classes in alphabetical order, showing
67
89
  # non-identifying role values as well
68
90
  def verbalise
69
91
  "Constellation over #{vocabulary.name}:\n" +
70
- vocabulary.object_type.keys.sort.map{|object_type|
92
+ vocabulary.object_type.keys.sort.map do |object_type|
71
93
  klass = vocabulary.const_get(object_type)
72
94
 
73
95
  # REVISIT: It would be better not to rely on the role name pattern here:
@@ -78,7 +100,7 @@ module ActiveFacts
78
100
  instances = send(object_type.to_sym)
79
101
  next nil unless instances.size > 0
80
102
  "\tEvery #{object_type}:\n" +
81
- instances.map{|key, instance|
103
+ instances.map do |key, instance|
82
104
  s = "\t\t" + instance.verbalise
83
105
  if (single_roles.size > 0)
84
106
  role_values =
@@ -93,8 +115,8 @@ module ActiveFacts
93
115
  s += " where " + role_values*", " if role_values.size > 0
94
116
  end
95
117
  s
96
- } * "\n"
97
- }.compact*"\n"
118
+ end * "\n"
119
+ end.compact*"\n"
98
120
  end
99
121
 
100
122
  # This method removes the given instance from this constellation's indexes
@@ -108,19 +130,29 @@ module ActiveFacts
108
130
  # If mandatory on the counterpart side, this may/must propagate the delete (without mutual recursion!)
109
131
  end
110
132
 
111
- # With parameters, assert an instance of the object_type whose name is the missing method, identified by the values passed as *args*.
133
+ # If a missing method is the name of a class in the vocabulary module for this constellation,
134
+ # then we want to access the collection of instances of that class, and perhaps assert new ones.
112
135
  # With no parameters, return the collection of all instances of that object_type.
113
- def method_missing(m, *args)
114
- if klass = @vocabulary.const_get(m)
115
- if args.size == 0
116
- # Return the collection of all instances of this class in the constellation:
117
- @instances[klass]
118
- else
119
- # Assert a new ground fact (object_type instance) of the specified class, identified by args:
120
- # REVISIT: create a constructor method here instead?
121
- instance, key = klass.assert_instance(self, args)
122
- instance
123
- end
136
+ # With parameters, assert an instance of the object_type identified by the values passed as args.
137
+ def method_missing(m, *args, &b)
138
+ if klass = @vocabulary.const_get(m) and klass.is_a?(Class) and klass.respond_to?(:assert_instance)
139
+
140
+ (class << self; self; end).
141
+ send(:define_method, sym = m.to_sym) do |*args|
142
+ instance_index = @instances[klass]
143
+ if args.size == 0
144
+ # Return the collection of all instances of this class in the constellation:
145
+ instance_index
146
+ else
147
+ # Assert a new ground fact (object_type instance) of the specified class, identified by args:
148
+ instance_index.assert(*args)
149
+ end
150
+ end
151
+
152
+ # This is the last time it'll be missing, so call it.
153
+ send(sym, *args, &b)
154
+ else
155
+ super
124
156
  end
125
157
  end
126
158
  end
@@ -19,53 +19,71 @@ module ActiveFacts
19
19
  # A value must be provided for every identifying role, but if the
20
20
  # last argument is a hash, they may come from there.
21
21
  #
22
- # Any additional (non-identifying) roles may also be passed in the final hash.
22
+ # If a supertype (including a secondary supertype) has a different
23
+ # identifier, the identifying roles must be provided in the hash.
24
+ #
25
+ # Any additional (non-identifying) roles in the hash are ignored
23
26
  def initialize(*args)
24
- super(args)
25
27
  klass = self.class
26
- hash = {}
27
- hash = args.pop.clone if Hash === args[-1]
28
+ while klass.identification_inherited_from
29
+ klass = klass.superclass
30
+ end
31
+
32
+ # if (o = klass.overrides_identification_of and !(o.identifying_role_names-klass.identifying_role_names).empty?)
33
+ # This is a class which must initialise its superclass' identifying roles
34
+ # The hash can provide the values, but those values must already be asserted
35
+ # in the constellation this object will exist in, since they won't get
36
+ # attached to/cloned into that constellation merely by being assigned here.
37
+ # REVISIT: Nothing takes care of that, currently.
38
+ #
39
+ # The solution to this is to have an empty initialize, add the new instance
40
+ # to the Constellation, then initialise_roles using normal assignment.
41
+ # end
42
+
43
+ hash = args[-1].is_a?(Hash) ? args.pop.clone : nil
44
+
45
+ # Pass just the hash, if there is one, else no arguments:
46
+ super(*(hash ? [hash] : []))
28
47
 
29
48
  # Pick any missing identifying roles out of the hash if possible:
30
- while args.size < (ir = klass.identifying_role_names).size
31
- value = hash[role = ir[args.size]]
49
+ irns = klass.identifying_role_names
50
+ while hash && args.size < irns.size
51
+ value = hash[role = irns[args.size]]
32
52
  hash.delete(role)
33
53
  args.push value
34
54
  end
35
55
 
36
- # If one arg is expected but more are passed, they might be the args for the object that plays the identifying role:
56
+ # If one arg is expected but more are passed, they might be the
57
+ # args for the object that plays a single identifying role:
37
58
  args = [args] if klass.identifying_role_names.size == 1 && args.size > 1
38
59
 
39
- # This should now only occur when there are too many args passed:
60
+ # This occur when there are too many args passed, or too few
61
+ # and no hash. Otherwise the missing ones will be nil.
40
62
  raise "Wrong number of parameters to #{klass}.new, " +
41
63
  "expect (#{klass.identifying_role_names*","}) " +
42
64
  "got (#{args.map{|a| a.to_s.inspect}*", "})" if args.size != klass.identifying_role_names.size
43
65
 
44
- # Assign the identifying roles in order, then the other roles passed as a hash:
45
- (klass.identifying_role_names.zip(args) + hash.entries).each do |role_name, value|
46
- role = klass.roles(role_name)
47
- send("#{role_name}=", value)
66
+ # Assign the identifying roles in order. Any other roles will be assigned by our caller
67
+ klass.identifying_role_names.zip(args).each do |role_name, value|
68
+ role = self.class.roles(role_name)
69
+ send(role.setter, value)
48
70
  end
49
71
  end
50
72
 
51
73
  def inspect #:nodoc:
52
- "\#<#{
53
- self.class.basename
54
- }:#{
55
- object_id
56
- }#{
57
- constellation ? " in #{constellation.inspect}" : ""
58
- } #{
59
- # REVISIT: Where there are one-to-one roles, this cycles
60
- self.class.identifying_role_names.map{|role| "@#{role}="+send(role).inspect }*" "
61
- }>"
74
+ inc = constellation ? " in #{constellation.inspect}" : ""
75
+ # REVISIT: Where there are one-to-one roles, this cycles
76
+ irnv = self.class.identifying_role_names.map do |role_name|
77
+ "@#{role_name}="+send(role_name).inspect
78
+ end
79
+ "\#<#{self.class.basename}:#{object_id}#{inc} #{ irnv*' ' }>"
62
80
  end
63
81
 
64
82
  # When used as a hash key, the hash key of this entity instance is calculated
65
83
  # by hashing the values of its identifying roles
66
84
  def hash
67
- self.class.identifying_role_names.map{|role|
68
- instance_variable_get("@#{role}")
85
+ self.class.identifying_role_names.map{|role_name|
86
+ instance_variable_get("@#{role_name}")
69
87
  }.inject(0) { |h,v|
70
88
  h ^= v.hash
71
89
  h
@@ -76,80 +94,89 @@ module ActiveFacts
76
94
  # comparing the values of its identifying roles
77
95
  def eql?(other)
78
96
  return false unless self.class == other.class
79
- self.class.identifying_role_names.each{|role|
80
- return false unless send(role).eql?(other.send(role))
97
+ self.class.identifying_role_names.each{|role_name|
98
+ return false unless send(role_name).eql?(other.send(role_name))
81
99
  }
82
100
  return true
83
101
  end
84
102
 
85
103
  # Verbalise this entity instance
86
104
  def verbalise(role_name = nil)
87
- "#{role_name || self.class.basename}(#{
88
- self.class.identifying_role_names.map{|role_sym|
89
- value = send(role_sym)
90
- role_name = self.class.roles(role_sym).name.to_s.camelcase
91
- value ? value.verbalise(role_name) : "nil"
92
- }*", "
93
- })"
105
+ irnv = self.class.identifying_role_names.map do |role_sym|
106
+ value = send(role_sym)
107
+ identifying_role_name = self.class.roles(role_sym).name.to_s.camelcase
108
+ value ? value.verbalise(identifying_role_name) : "nil"
109
+ end
110
+ "#{role_name || self.class.basename}(#{ irnv*', ' })"
94
111
  end
95
112
 
96
113
  # Return the array of the values of this entity instance's identifying roles
97
114
  def identifying_role_values
98
- self.class.identifying_role_names.map{|role|
99
- send(role)
100
- }
115
+ self.class.identifying_role_names.map do |role_name|
116
+ send(role_name).identifying_role_values
117
+ end
101
118
  end
102
119
 
103
120
  # All classes that become Entity types receive the methods of this class as class methods:
104
121
  module ClassMethods
105
122
  include Instance::ClassMethods
106
123
 
124
+ attr_accessor :identification_inherited_from
125
+ attr_accessor :overrides_identification_of
126
+
107
127
  # Return the array of Role objects that define the identifying relationships of this Entity type:
108
128
  def identifying_role_names
109
- @identifying_role_names ||= []
129
+ if identification_inherited_from
130
+ superclass.identifying_role_names
131
+ else
132
+ @identifying_role_names ||= []
133
+ end
110
134
  end
111
135
 
112
136
  def identifying_roles
113
- debug :persistence, "Identifying roles for #{basename}" do
114
- @identifying_role_names.map{|name|
115
- role = roles[name] || (!superclass.is_entity_type || superclass.roles[name])
116
- debug :persistence, "#{name} -> #{role ? "found" : "NOT FOUND"}"
137
+ # REVISIT: Should this return nil if identification_inherited_from?
138
+ @identifying_roles ||=
139
+ identifying_role_names.map do |role_name|
140
+ role = roles[role_name] || (!superclass.is_entity_type || superclass.roles[role_name])
117
141
  role
118
- }
119
- end
142
+ end
120
143
  end
121
144
 
122
- # Convert the passed arguments into an array of Instance objects that can identify an instance of this Entity type:
145
+ # Convert the passed arguments into an array of raw values (or arrays of values, transitively)
146
+ # that identify an instance of this Entity type:
123
147
  def identifying_role_values(*args)
124
- #puts "Getting identifying role values #{identifying_role_names.inspect} of #{basename} using #{args.inspect}"
148
+ irns = identifying_role_names
125
149
 
126
150
  # If the single arg is an instance of the correct class or a subclass,
127
151
  # use the instance's identifying_role_values
128
- if (args.size == 1 and
129
- (arg = args[0]).is_a?(self)) # REVISIT: or a secondary supertype
130
- arg = arg.__getobj__ if RoleProxy === arg
131
- return arg.identifying_role_values
152
+ has_hash = args[-1].is_a?(Hash)
153
+ if (args.size == 1+(has_hash ?1:0) and (arg = args[0]).is_a?(self))
154
+ # With a secondary supertype or a subtype having separate identification,
155
+ # we would get the wrong identifier from arg.identifying_role_values:
156
+ return irns.map do |role_name|
157
+ # Use the identifier for the class expected, not the actual:
158
+ value = arg.send(role_name)
159
+ value && arg.class.roles(role_name).counterpart_object_type.identifying_role_values(value)
160
+ end
132
161
  end
133
162
 
134
- ir = identifying_role_names
135
- args, arg_hash = ActiveFacts::extract_hash_args(ir, args)
163
+ args, arg_hash = ActiveFacts::extract_hash_args(irns, args)
136
164
 
137
- if args.size > ir.size
138
- raise "You've provided too many values for the identifier of #{basename}, which expects (#{ir*', '})"
165
+ if args.size > irns.size
166
+ raise "You've provided too many values for the identifier of #{basename}, which expects (#{irns*', '})"
139
167
  end
140
168
 
141
- role_args = ir.map{|role_sym| roles(role_sym)}.zip(args)
169
+ role_args = irns.map{|role_sym| roles(role_sym)}.zip(args)
142
170
  role_args.map do |role, arg|
143
- #puts "Getting identifying_role_value for #{role.counterpart_object_type.basename} using #{arg.inspect}"
144
171
  next !!arg unless role.counterpart # Unary
145
- arg = arg.__getobj__ if RoleProxy === arg
146
- if arg.is_a?(role.counterpart_object_type) # REVISIT: or a secondary supertype
147
- # Note that with a secondary supertype, it must still return the values of these identifying_role_names
148
- next arg.identifying_role_values
172
+ if arg.is_a?(role.counterpart.object_type) # includes secondary supertypes
173
+ # With a secondary supertype or a type having separate identification,
174
+ # we would get the wrong identifier from arg.identifying_role_values:
175
+ next role.counterpart_object_type.identifying_role_values(arg)
149
176
  end
150
177
  if arg == nil # But not false
151
178
  if role.mandatory
152
- raise "You must provide a #{role.counterpart_object_type.name} to identify a #{basename}"
179
+ raise "You must provide a #{role.counterpart.object_type.name} to identify a #{basename}"
153
180
  end
154
181
  else
155
182
  role.counterpart_object_type.identifying_role_values(*arg)
@@ -157,6 +184,10 @@ module ActiveFacts
157
184
  end
158
185
  end
159
186
 
187
+ # REVISIT: This method should verify that all identifying roles (including
188
+ # those required to identify any superclass) are present (if mandatory)
189
+ # and are unique... BEFORE it creates any new object(s)
190
+ # This is a hard problem because it's recursive.
160
191
  def assert_instance(constellation, args) #:nodoc:
161
192
  # Build the key for this instance from the args
162
193
  # The key of an instance is the value or array of keys of the identifying values.
@@ -166,37 +197,55 @@ module ActiveFacts
166
197
  # Find and return an existing instance matching this key
167
198
  instances = constellation.instances[self] # All instances of this class in this constellation
168
199
  instance = instances[key]
169
- # DEBUG: puts "assert #{self.basename} #{key.inspect} #{instance ? "exists" : "new"}"
200
+ # REVISIT: This ignores any additional attribute assignments
170
201
  return instance, key if instance # A matching instance of this class
171
202
 
172
203
  # Now construct each of this object's identifying roles
173
- ir = identifying_role_names
174
- args, arg_hash = ActiveFacts::extract_hash_args(ir, args)
175
- role_values = ir.map{|role_sym| roles(role_sym)}.zip(args)
176
- key = [] # Gather the actual key (AutoCounters are special)
177
- values = role_values.map do |role, arg|
178
- if !arg
179
- value = role_key = nil # No value
180
- elsif !role.counterpart
181
- value = role_key = !!arg # Unary
182
- elsif arg.is_a?(role.counterpart_object_type) # REVISIT: or a secondary supertype
183
- arg = arg.__getobj__ if RoleProxy === arg
184
- raise "Connecting values across constellations" unless arg.constellation == constellation
185
- value, role_key = arg, arg.identifying_role_values
186
- else
187
- value, role_key = role.counterpart_object_type.assert_instance(constellation, Array(arg))
204
+ irns = identifying_role_names
205
+
206
+ has_hash = args[-1].is_a?(Hash)
207
+ if args.size == 1+(has_hash ?1:0) and args[0].is_a?(self)
208
+ # We received a single argument of a compatible type
209
+ # With a secondary supertype or a type having separate identification,
210
+ # we would get the wrong identifier from arg.identifying_role_values:
211
+ key =
212
+ values = identifying_role_values(args[0])
213
+ values = values + [arg_hash = args.pop] if has_hash
214
+ else
215
+ args, arg_hash = ActiveFacts::extract_hash_args(irns, args)
216
+ roles_and_values = irns.map{|role_sym| roles(role_sym)}.zip(args)
217
+ key = [] # Gather the actual key (AutoCounters are special)
218
+ values = roles_and_values.map do |role, arg|
219
+ if role.unary?
220
+ # REVISIT: This could be absorbed into a special counterpart.object_type.assert_instance
221
+ value = role_key = arg ? true : arg # Preserve false and nil
222
+ elsif !arg
223
+ value = role_key = nil
224
+ else
225
+ #trace :assert, "Asserting #{role.counterpart.object_type} with #{Array(arg).inspect} for #{self}.#{role.name}" do
226
+ value, role_key = role.counterpart.object_type.assert_instance(constellation, Array(arg))
227
+ #end
228
+ end
229
+ key << role_key
230
+ value
188
231
  end
189
- key << role_key
190
- value
191
- end
192
- values << arg_hash if arg_hash and !arg_hash.empty?
232
+ values << arg_hash if arg_hash and !arg_hash.empty?
233
+ end
193
234
 
194
- #puts "Creating new #{basename} using #{values.inspect}"
195
- instance = new(*values)
235
+ #trace :assert, "Constructing new #{self} with #{values.inspect}" do
236
+ instance = new(*values)
237
+ #end
196
238
 
197
239
  # Make the new entity instance a member of this constellation:
198
240
  instance.constellation = constellation
199
- return *index_instance(instance, key, ir)
241
+
242
+ # Now assign any extra args in the hash which weren't identifiers (extra identifiers will be assigned again)
243
+ (arg_hash ? arg_hash.entries : []).each do |role_name, value|
244
+ role = roles(role_name)
245
+ instance.send(role.setter, value)
246
+ end
247
+
248
+ return *index_instance(instance, key, irns)
200
249
  end
201
250
 
202
251
  def index_instance(instance, key = nil, key_roles = nil) #:nodoc:
@@ -205,12 +254,12 @@ module ActiveFacts
205
254
  key = (key_roles = identifying_role_names).map do |role_name|
206
255
  instance.send role_name
207
256
  end
257
+ raise "You must pass values for #{key_roles.inspect} to identify a #{self.name}" if key.compact == []
208
258
  end
209
259
 
210
260
  # Index the instance for this class in the constellation
211
261
  instances = instance.constellation.instances[self]
212
262
  instances[key] = instance
213
- # DEBUG: puts "indexing entity #{basename} using #{key.inspect} in #{constellation.object_id}"
214
263
 
215
264
  # Index the instance for each supertype:
216
265
  supertypes.each do |supertype|
@@ -223,18 +272,28 @@ module ActiveFacts
223
272
  # A object_type that isn't a ValueType must have an identification scheme,
224
273
  # which is a list of roles it plays. The identification scheme may be
225
274
  # inherited from a superclass.
226
- def initialise_entity_type(*args) #:nodoc:
227
- #puts "Initialising entity type #{self} using #{args.inspect}"
228
- @identifying_role_names = superclass.identifying_role_names if superclass.is_entity_type
229
- # REVISIT: @identifying_role_names here are the symbols passed in, not the Role objects we should use.
230
- # We'd need late binding to use Role objects...
231
- @identifying_role_names = args if args.size > 0 || !@identifying_role_names
275
+ def identified_by(*args) #:nodoc:
276
+ raise "You must list the roles which will identify #{self.basename}" unless args.size > 0
277
+
278
+ # Catch the case where we state the same identification as our superclass:
279
+ inherited_role_names = identifying_role_names
280
+ if !inherited_role_names.empty?
281
+ self.overrides_identification_of = superclass
282
+ while from = self.overrides_identification_of.identification_inherited_from
283
+ self.overrides_identification_of = from
284
+ end
285
+ end
286
+ return if inherited_role_names == args
287
+ self.identification_inherited_from = nil
288
+
289
+ # @identifying_role_names here are the symbols passed in, not the Role
290
+ # objects we should use. We'd need late binding to use Role objects...
291
+ @identifying_role_names = args
232
292
  end
233
293
 
234
294
  def inherited(other) #:nodoc:
235
- other.identified_by *identifying_role_names
295
+ other.identification_inherited_from = self
236
296
  subtypes << other unless subtypes.include? other
237
- #puts "#{self.name} inherited by #{other.name}"
238
297
  vocabulary.__add_object_type(other)
239
298
  end
240
299
 
@@ -249,7 +308,6 @@ module ActiveFacts
249
308
 
250
309
  # Register ourselves with the parent module, which has become a Vocabulary:
251
310
  vocabulary = other.modspace
252
- # puts "Entity.included(#{other.inspect})"
253
311
  unless vocabulary.respond_to? :object_type # Extend module with Vocabulary if necessary
254
312
  vocabulary.send :extend, Vocabulary
255
313
  end