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