activefacts-api 1.1.0 → 1.3.0

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.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- ZjdhYmJiODJmYjRmM2IwYjE5MDUyOWEzYTk2ZTlhZTNiOTlkYTZjMg==
4
+ NjExZjgzZWEyZGI1Njg0ZGE5Mzc1NmU1M2RhNWRmYTQwY2Y5ODAxMg==
5
5
  data.tar.gz: !binary |-
6
- MGE3OGQ5OWY4MThhMzUyYzE3NmU3NDJkMWMwZWJhNWQyMjE1OTVkZQ==
6
+ YWUzNGNkMmEyOWQ4YWNjNDI0OWUyOTZkZTJhYzBhYTdmNGYxMmRkOA==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- M2FiZDNkMzA5YTcxOTQyOGNmODBhZDI4YjgwNTBmOTM1NmViMDhiMjcxNDhk
10
- Mjg0Njg4NGM3OWQ0YjIzN2IyOWU0MzAwYzMzMmU1N2IxMDg1YjU2ZTdmMDNk
11
- N2ViMjQyMmYwYmM1YzhlZjNlZGUxZTI5OTNmNjAzNzhhYmY3MGU=
9
+ MDYwZmQ4ZDU4ZGNiMDZmZjNlYTUzZDc4M2JlMThlM2Y0YzMzNWRmMjdiZGYw
10
+ ZDJjM2U5ZTY4NDMzNjNkYzE3Njk0NDA4ZTMwMTZjYjI3Y2JkNzM3N2M5NTVk
11
+ NThlMjM4MjE5ZDA2YzljMmJkYTk0OTdmNDMxMWRjODM5Y2IwMTA=
12
12
  data.tar.gz: !binary |-
13
- YTRiY2IyMTgzMmQwYTU5ODhjZTcwNDE4MDUzMjVhYjllNTAxOTc1ZjFhNzFm
14
- NjhhNmRiMGEzZGUwYjgxNTI4OWZhODQ4MGRiYmQ5NTMzNDUxOGY4NDY4ODVj
15
- ZGE4NWM3NzY0Njc1MDc0NmMzYmM3NDNkOTdjNzVmOTMxNDI4ODU=
13
+ NDdjZmU0ZDVlNzJkY2Y5OTI3ZGFiYzNkNWUxY2EyNjVjYTJkODdhZDBmYWE2
14
+ ZDY3N2E1YTQzM2E4NDk0MjY2N2YwNmFlNjk3MmVjZTgxZjg0MDJhN2Y4Y2Jh
15
+ ZjFiMjEwZjNjMjc1NmI5ODI2MDJhYmM0M2I4MWRkZDdlMGZlZjY=
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.0
1
+ 1.3.0
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: activefacts-api 1.1.0 ruby lib
5
+ # stub: activefacts-api 1.3.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "activefacts-api"
9
- s.version = "1.1.0"
9
+ s.version = "1.3.0"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib"]
13
13
  s.authors = ["Clifford Heath"]
14
- s.date = "2014-08-12"
14
+ s.date = "2014-08-22"
15
15
  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"
16
16
  s.email = "clifford.heath@gmail.com"
17
17
  s.extra_rdoc_files = [
@@ -34,6 +34,69 @@ module ActiveFacts
34
34
  class Constellation
35
35
  attr_reader :vocabulary
36
36
 
37
+ # Create a new empty Constellation over the given Vocabulary
38
+ def initialize(vocabulary, options = {})
39
+ @vocabulary = vocabulary
40
+ @options = options
41
+ end
42
+
43
+ def assert(klass, *args)
44
+ with_candidates do
45
+ klass.assert_instance self, args
46
+ end
47
+ end
48
+
49
+ # Delete instances from the constellation, nullifying (or cascading) the roles each plays
50
+ def retract(*instances)
51
+ Array(instances).each do |i|
52
+ i.retract
53
+ end
54
+ self
55
+ end
56
+
57
+ # Evaluate assertions against the population of this Constellation
58
+ def populate &block
59
+ instance_eval(&block)
60
+ self
61
+ end
62
+
63
+ # If a missing method is the name of a class in the vocabulary module for this constellation,
64
+ # then we want to access the collection of instances of that class, and perhaps assert new ones.
65
+ # With no parameters, return the collection of all instances of that object_type.
66
+ # With parameters, assert an instance of the object_type identified by the values passed as args.
67
+ def method_missing(m, *args, &b)
68
+ klass = @vocabulary.const_get(m)
69
+ if invalid_object_type klass
70
+ super
71
+ else
72
+ define_class_accessor m, klass
73
+ send(m, *args, &b)
74
+ end
75
+ end
76
+
77
+ def define_class_accessor m, klass
78
+ (class << self; self; end).
79
+ send(:define_method, m) do |*args|
80
+ if args.size == 0
81
+ # Return the collection of all instances of this class in the constellation:
82
+ instances[klass]
83
+ else
84
+ # Assert a new ground fact (object_type instance) of the specified class, identified by args:
85
+ assert(klass, *args)
86
+ end
87
+ end
88
+ end
89
+
90
+ def inspect #:nodoc:
91
+ total = instances.values.map(&:size).inject(:+) || 0
92
+ "Constellation:#{object_id} over #{@vocabulary.name} with #{total}/#{instances.size}"
93
+ end
94
+
95
+ # Loggers is an array of callbacks
96
+ def loggers
97
+ @loggers ||= []
98
+ end
99
+
37
100
  def invalid_object_type klass
38
101
  case
39
102
  when !klass.is_a?(Class)
@@ -83,6 +146,7 @@ module ActiveFacts
83
146
  @on_admission = nil
84
147
  candidates.each do |instance|
85
148
  instance.class.index_instance(self, instance)
149
+ loggers.each{|l| l.call(:assert, instance.class, instance.identifying_role_values)}
86
150
  end
87
151
  on_admission.each do |b|
88
152
  b.call
@@ -108,32 +172,6 @@ module ActiveFacts
108
172
  @candidates && @candidates.detect{|c| c.is_a?(klass) && c.identifying_role_values(klass) == key }
109
173
  end
110
174
 
111
- # Create a new empty Constellation over the given Vocabulary
112
- def initialize(vocabulary, options = {})
113
- @vocabulary = vocabulary
114
- @options = options
115
- end
116
-
117
- def assert(klass, *args)
118
- with_candidates do
119
- klass.assert_instance self, args
120
- end
121
- end
122
-
123
- # Evaluate assertions against the population of this Constellation
124
- def populate &block
125
- instance_eval(&block)
126
- self
127
- end
128
-
129
- # Delete instances from the constellation, nullifying (or cascading) the roles each plays
130
- def retract(*instances)
131
- Array(instances).each do |i|
132
- i.retract
133
- end
134
- self
135
- end
136
-
137
175
  # This method removes the given instance from this constellation's indexes
138
176
  # It must be called before the identifying roles get deleted or nullified.
139
177
  def deindex_instance(instance) #:nodoc:
@@ -151,37 +189,6 @@ module ActiveFacts
151
189
  end
152
190
  end
153
191
 
154
- def define_class_accessor m, klass
155
- (class << self; self; end).
156
- send(:define_method, m) do |*args|
157
- if args.size == 0
158
- # Return the collection of all instances of this class in the constellation:
159
- instances[klass]
160
- else
161
- # Assert a new ground fact (object_type instance) of the specified class, identified by args:
162
- assert(klass, *args)
163
- end
164
- end
165
- end
166
-
167
- # If a missing method is the name of a class in the vocabulary module for this constellation,
168
- # then we want to access the collection of instances of that class, and perhaps assert new ones.
169
- # With no parameters, return the collection of all instances of that object_type.
170
- # With parameters, assert an instance of the object_type identified by the values passed as args.
171
- def method_missing(m, *args, &b)
172
- klass = @vocabulary.const_get(m)
173
- if invalid_object_type klass
174
- super
175
- else
176
- define_class_accessor m, klass
177
- send(m, *args, &b)
178
- end
179
- end
180
-
181
- def inspect #:nodoc:
182
- "Constellation:#{object_id}"
183
- end
184
-
185
192
  # Constellations verbalise all members of all classes in alphabetical order, showing
186
193
  # non-identifying role values as well
187
194
  def verbalise
@@ -27,39 +27,41 @@ module ActiveFacts
27
27
  # here.
28
28
  def initialize(arg_hash)
29
29
  raise ArgumentError.new("#{self}.new expects a hash. You should use assert instead anyhow") unless arg_hash.is_a?(Hash)
30
+ super(arg_hash) # Initialise the Instance
31
+ initialize_existential_roles(self.class, arg_hash)
32
+ end
30
33
 
31
- super(arg_hash)
34
+ def initialize_existential_roles(klass, arg_hash)
35
+ # If overrides_identification_of, assign those attributes too (recursively)
36
+ if o = klass.overrides_identification_of
37
+ initialize_existential_roles(o, arg_hash)
38
+ end
32
39
 
33
- unless (klass = self.class).identification_inherited_from
34
- irns = klass.identifying_role_names
35
- irns.each do |role_name|
36
- role = klass.all_role(role_name)
37
- key = arg_hash.delete(role_name)
38
- value =
39
- if key == nil
40
- nil
41
- elsif role.unary?
42
- (key && true) # Preserve nil and false
43
- else
44
- role.counterpart.object_type.assert_instance(constellation, Array(key))
45
- end
40
+ irns = klass.identifying_role_names
41
+ irns.each do |role_name|
42
+ role = klass.all_role(role_name)
43
+ key = arg_hash.delete(role_name)
44
+ value =
45
+ if key == nil
46
+ nil
47
+ elsif role.unary?
48
+ (key && true) # Preserve nil and false
49
+ else
50
+ role.counterpart.object_type.assert_instance(constellation, Array(key))
51
+ end
46
52
 
47
- begin
48
- @constellation.send(:instance_variable_set, :@suspend_duplicate_key_check, true)
49
- send(role.setter, value)
50
- @constellation.send(:instance_variable_set, :@suspend_duplicate_key_check, false)
51
- # rescue NoMethodError => e
52
- # raise settable_roles_exception(e, role_name)
53
+ begin
54
+ unless instance_variable_get(role.variable) != nil # Not if it was set by a superclass identifier
55
+ send(role.setter, value, ObjectType::CHECKED_IDENTIFYING_ROLE)
53
56
  end
54
- # instance_variable_set(role.setter, value)
57
+ rescue NoMethodError => e
58
+ raise settable_roles_exception(e, role_name)
55
59
  end
56
- end
60
+ end
57
61
  end
58
62
 
59
- =begin
60
- # I forget how it was possible to reproduce this exception,
61
- # so I can't get code coverage over it. It might not be still possible,
62
- # but I can't be sure so I'll leave the code here for now.
63
+ # This exception is raised when an entity is instantiated before the
64
+ # object types which play its identifying roles is defined.
63
65
  def settable_roles_exception e, role_name
64
66
  n = NoMethodError.new(
65
67
  "You cannot assert a #{self.class} until you define #{role_name}.\n" +
@@ -86,7 +88,6 @@ module ActiveFacts
86
88
  end.
87
89
  flatten
88
90
  end
89
- =end
90
91
 
91
92
  public
92
93
  def inspect #:nodoc:
@@ -208,7 +209,6 @@ module ActiveFacts
208
209
  # We need to check each superclass that has a different identification pattern
209
210
  def check_identification_change_legality(role, value)
210
211
  return unless @constellation && role.is_identifying
211
- return if @constellation.send(:instance_variable_get, :@suspend_duplicate_key_check)
212
212
 
213
213
  klasses = [self.class] + self.class.supertypes_transitive
214
214
  last_identity = nil
@@ -85,7 +85,7 @@ module ActiveFacts
85
85
  def initialize(klass, role_name, value)
86
86
  super("Illegal attempt to assert #{klass.basename} having identifying value" +
87
87
  " (#{role_name} is #{value.verbalise})," +
88
- " when #{value.related_entities(false).map(&:verbalise).join(", ")} already exists")
88
+ " when #{value.respond_to?(:related_entities) ? value.related_entities(false).map(&:verbalise).join(", ") : 'an equivalent entity'} already exists")
89
89
  end
90
90
  end
91
91
 
@@ -1,22 +1,41 @@
1
1
  require 'delegate'
2
2
  require 'securerandom'
3
3
 
4
+ def SecureRandom.format_uuid hex32
5
+ hex32.sub(
6
+ @@format_pattern ||= /(........)(....)(....)(....)(............)/,
7
+ @@format_string ||= '\1-\2-\3-\4-\5'
8
+ )
9
+ end
10
+
4
11
  unless defined? SecureRandom.uuid
5
12
  # I think this only applies to 1.8.6 (and JRuby/Rubinius in 1.8 mode) now:
6
13
  def SecureRandom.uuid
7
- hex(16).
8
- sub(
9
- @@format_pattern ||= /(........)(....)(....)(....)(............)/,
10
- @@format_string ||= '\1-\2-\3-\4-\5'
11
- )
14
+ format_uuid(hex(16))
12
15
  end
13
16
  end
14
17
 
15
18
  # The Guid class is what it says on the packet, but you can assert a :new one.
16
19
  class Guid
20
+ SEQ_FILE_NAME = "/tmp/ActiveFactsRandom"
21
+ @@sequence = nil
17
22
  def initialize(i = :new)
23
+ @@sequence = ENV['ACTIVEFACTS_RANDOM'] || false if @@sequence == nil
18
24
  if i == :new
19
- @value = SecureRandom.uuid.freeze
25
+ case @@sequence
26
+ when 'fixed'
27
+ @@counter ||= 0
28
+ @value = SecureRandom.format_uuid('%032x' % (@@counter += 1))
29
+ when 'record'
30
+ @@sequence_file ||= File.open(SEQ_FILE_NAME, 'w')
31
+ @value = SecureRandom.uuid.freeze
32
+ @@sequence_file.puts(@value)
33
+ when 'replay'
34
+ @@sequence_file ||= File.open(SEQ_FILE_NAME, 'r')
35
+ @value = @@sequence_file.gets.chomp
36
+ else
37
+ @value = SecureRandom.uuid.freeze
38
+ end
20
39
  elsif (v = i.to_s).length == 36 and !(v !~ /[^0-9a-f]/i)
21
40
  @value = v.clone.freeze
22
41
  else
@@ -52,8 +52,25 @@ module ActiveFacts
52
52
 
53
53
  # De-assign all functional roles and remove from constellation, if any.
54
54
  def retract
55
+ return unless constellation = @constellation
56
+
57
+ unless constellation.loggers.empty?
58
+ # An object may have multiple identifiers, with potentially overlapping role sets
59
+ # Get one copy of each role to use in asserting the instance
60
+ if self.class.is_entity_type
61
+ identifying_role_values = {}
62
+ ([self.class]+self.class.supertypes_transitive).each do |klass|
63
+ klass.identifying_role_names.zip(identifying_role_values(klass)).each do |name, value|
64
+ identifying_role_values[name] = value
65
+ end
66
+ end
67
+ else
68
+ identifying_role_values = self
69
+ end
70
+ end
71
+
55
72
  # Delete from the constellation first, while we remember our identifying role values
56
- @constellation.deindex_instance(self) if @constellation
73
+ constellation.deindex_instance(self)
57
74
  instance_variable_set(@@constellation_variable_name ||= "@constellation", nil)
58
75
 
59
76
  # Now, for all roles (from this class and all supertypes), assign nil to all functional roles
@@ -128,6 +145,9 @@ module ActiveFacts
128
145
  end
129
146
  end
130
147
  end
148
+
149
+ constellation.loggers.each{|l| l.call(:retract, self.class, identifying_role_values) }
150
+
131
151
  end
132
152
 
133
153
  module ClassMethods #:nodoc:
@@ -11,6 +11,9 @@ module ActiveFacts
11
11
 
12
12
  # ObjectType contains methods that are added as class methods to all Value and Entity classes.
13
13
  module ObjectType
14
+ SKIP_MUTUAL_PROPAGATION = 0x1
15
+ CHECKED_IDENTIFYING_ROLE = 0x2
16
+
14
17
  # What vocabulary (Ruby module) does this object_type belong to?
15
18
  def vocabulary
16
19
  modspace # The module that contains this object_type.
@@ -231,16 +234,38 @@ module ActiveFacts
231
234
  end
232
235
 
233
236
  def define_unary_role_accessor(role)
234
- define_method role.setter do |value|
235
- assigned = case value
237
+ define_method role.setter do |value, options = 0|
238
+ # Normalise the value to be assigned (nil, false, true):
239
+ value = case value
236
240
  when nil; nil
237
241
  when false; false
238
242
  else true
239
243
  end
240
- instance_variable_set(role.variable, assigned)
241
- # REVISIT: Consider whether we want to provide a way to find all instances playing/not playing this boolean role
242
- # Analogous to true.all_thing_as_role_name...
243
- assigned
244
+
245
+ old = instance_variable_get(role.variable)
246
+ return value if old == value
247
+
248
+ if role.is_identifying and (options&CHECKED_IDENTIFYING_ROLE) == 0
249
+ check_identification_change_legality(role, value)
250
+ impacts = analyse_impacts(role)
251
+ end
252
+
253
+ instance_variable_set(role.variable, value)
254
+
255
+ if impacts
256
+ @constellation.when_admitted do
257
+ # REVISIT: Consider whether we want to provide a way to find all instances
258
+ # playing/not playing this boolean role, analogous to true.all_thing_as_role_name...
259
+ apply_impacts(impacts) # Propagate dependent key changes
260
+ end
261
+ end
262
+
263
+ unless @constellation.loggers.empty? or options != 0
264
+ sv = self.identifying_role_values(role.object_type)
265
+ @constellation.loggers.each{|l| l.call(:assign, role.object_type, role, sv, old, value) }
266
+ end
267
+
268
+ value
244
269
  end
245
270
  define_single_role_getter(role)
246
271
  end
@@ -258,10 +283,7 @@ module ActiveFacts
258
283
  define_single_role_getter(role)
259
284
 
260
285
  # What I want is the following, but it doesn't work in Ruby 1.8
261
- # define_method role.setter do |value, mutual_propagation = true|
262
- define_method role.setter do |*a|
263
- value, mutual_propagation = *a
264
- mutual_propagation = true if a.size < 2
286
+ define_method role.setter do |value, options = 0|
265
287
  role_var = role.variable
266
288
 
267
289
  # Get old value, and jump out early if it's unchanged:
@@ -274,7 +296,8 @@ module ActiveFacts
274
296
  return value if old == value
275
297
  end
276
298
 
277
- if (role.is_identifying) # We're changing this object's key. Check legality and prepare to propagate
299
+ # We're changing this object's key. Check legality and prepare to propagate
300
+ if role.is_identifying and (options&CHECKED_IDENTIFYING_ROLE) == 0
278
301
  check_identification_change_legality(role, value)
279
302
 
280
303
  # puts "Starting to analyse impact of changing 1-1 #{role.inspect} to #{value.inspect}"
@@ -284,15 +307,24 @@ module ActiveFacts
284
307
  instance_variable_set(role_var, value)
285
308
 
286
309
  # Remove self from the old counterpart:
287
- old.send(role.counterpart.setter, nil, false) if old and mutual_propagation
310
+ if old and (options&SKIP_MUTUAL_PROPAGATION) == 0
311
+ old.send(role.counterpart.setter, nil, options|SKIP_MUTUAL_PROPAGATION)
312
+ end
288
313
 
289
314
  @constellation.when_admitted do
290
315
  # Assign self to the new counterpart
291
- value.send(role.counterpart.setter, self) if value
316
+ value.send(role.counterpart.setter, self, options) if value && (options&SKIP_MUTUAL_PROPAGATION) == 0
292
317
 
293
318
  apply_impacts(impacts) if impacts # Propagate dependent key changes
294
319
  end
295
320
 
321
+ unless @constellation.loggers.empty? or options != 0
322
+ sv = self.identifying_role_values(role.object_type)
323
+ ov = old.identifying_role_values
324
+ nv = value.identifying_role_values
325
+ @constellation.loggers.each{|l| l.call(:assign, role.object_type, role, sv, ov, nv) }
326
+ end
327
+
296
328
  value
297
329
  end
298
330
  end
@@ -300,11 +332,7 @@ module ActiveFacts
300
332
  def define_one_to_many_accessor(role)
301
333
  define_single_role_getter(role)
302
334
 
303
- # What I want is the following, but it doesn't work in Ruby 1.8
304
- # define_method role.setter do |value, mutual_propagation = true|
305
- define_method role.setter do |*a|
306
- value, mutual_propagation = *a
307
- mutual_propagation = true if a.size < 2
335
+ define_method role.setter do |value, options = 0|
308
336
  role_var = role.variable
309
337
 
310
338
  # Get old value, and jump out early if it's unchanged:
@@ -317,14 +345,15 @@ module ActiveFacts
317
345
  return value if old == value # Occurs when another instance having the same value is assigned
318
346
  end
319
347
 
320
- if (role.is_identifying) # We're changing this object's key. Check legality and prepare to propagate
348
+ if role.is_identifying and (options&CHECKED_IDENTIFYING_ROLE) == 0
349
+ # We're changing this object's key. Check legality and prepare to propagate
321
350
  check_identification_change_legality(role, value)
322
351
 
323
352
  # puts "Starting to analyse impact of changing 1-N #{role.inspect} to #{value.inspect}"
324
353
  impacts = analyse_impacts(role)
325
354
  end
326
355
 
327
- if old && mutual_propagation
356
+ if old and (options&SKIP_MUTUAL_PROPAGATION) == 0
328
357
  old_role_values = old.send(getter = role.counterpart.getter)
329
358
  old_key = old_role_values.index_values(self)
330
359
  end
@@ -341,11 +370,21 @@ module ActiveFacts
341
370
 
342
371
  @constellation.when_admitted do
343
372
  # Add "self" into the counterpart
344
- value.send(getter ||= role.counterpart.getter).add_instance(self, identifying_role_values(role.object_type)) if value
373
+ if value
374
+ rv = value.send(getter ||= role.counterpart.getter)
375
+ rv.add_instance(self, identifying_role_values(role.object_type))
376
+ end
345
377
 
346
378
  apply_impacts(impacts) if impacts # Propagate dependent key changes
347
379
  end
348
380
 
381
+ unless @constellation.loggers.empty? or options != 0
382
+ sv = self.identifying_role_values(role.object_type)
383
+ ov = old.identifying_role_values
384
+ nv = value.identifying_role_values
385
+ @constellation.loggers.each{|l| l.call(:assign, role.object_type, role, sv, ov, nv) }
386
+ end
387
+
349
388
  value
350
389
  end
351
390
  end
@@ -361,12 +400,12 @@ module ActiveFacts
361
400
  if role.counterpart and
362
401
  counterpart = role.counterpart.object_type and
363
402
  counterpart.is_entity_type
364
- index_roles = counterpart.identifying_roles - [role.counterpart]
403
+ excluded_role = counterpart.identifying_roles.index(role.counterpart)
365
404
  else
366
405
  index_roles = nil
367
406
  end
368
407
 
369
- instance_variable_set(role_var, RoleValues.new(role.counterpart.object_type, index_roles))
408
+ instance_variable_set(role_var, RoleValues.new(role.counterpart, excluded_role))
370
409
  end
371
410
  # Look up a value by the key provided, or return the whole collection
372
411
  keys.size == 0 ? role_values : role_values.[](*keys)
@@ -8,15 +8,20 @@ module ActiveFacts
8
8
  module API
9
9
 
10
10
  class RoleValues #:nodoc:
11
- attr_accessor :object_type
11
+ attr_accessor :role
12
12
  attr_accessor :sort
13
13
  attr_accessor :index_roles
14
+ def object_type
15
+ @role.object_type
16
+ end
14
17
 
15
- def initialize object_type, index_roles = nil, sort = nil
16
- @object_type = object_type
17
- @sort = sort == nil ? API::sorted : !!sort
18
+ def initialize role, excluded_role = nil
19
+ @role = role
20
+ # Can't control sorting from the constructor API: @sort = sort == nil ? API::sorted : !!sort
21
+ @sort = API::sorted
22
+ @excluded_role = excluded_role
18
23
  @a = @sort ? RBTree.new : []
19
- @index_roles = index_roles
24
+ (@index_roles = role.object_type.identifying_roles.dup).delete_at(@excluded_role) if @excluded_role
20
25
  end
21
26
 
22
27
  def +(a)
@@ -60,7 +65,10 @@ module ActiveFacts
60
65
 
61
66
  def index_values object
62
67
  if @index_roles
63
- @index_roles.map{|r| object.send(r.name).identifying_role_values}
68
+ @index_roles.map{|r|
69
+ role_value = object.send(r.name)
70
+ role_value.identifying_role_values((c = r.counterpart) ? c.object_type : role_value.class)
71
+ }
64
72
  else
65
73
  object.identifying_role_values
66
74
  end
@@ -68,7 +76,10 @@ module ActiveFacts
68
76
 
69
77
  def add_instance(value, key)
70
78
  if @sort
71
- @a[form_key(index_values(value))] = value
79
+ # Exclude the excluded role, if any:
80
+ (key = key.dup).delete_at(@excluded_role) if @excluded_role
81
+ @a[form_key(key)] = value
82
+ # Old slow way: @a[form_key(index_values(value))] = value
72
83
  else
73
84
  @a << value
74
85
  end
@@ -433,5 +433,9 @@ describe "An instance of every type of ObjectType" do
433
433
  t.is_ok.should == true
434
434
  f.is_ok.should == false
435
435
  s.is_ok.should == true
436
+
437
+ proc { n.is_ok = false }.should raise_error(ActiveFacts::API::DuplicateIdentifyingValueException)
438
+ proc { t.is_ok = nil }.should raise_error(ActiveFacts::API::DuplicateIdentifyingValueException)
439
+ proc { f.is_ok = true }.should raise_error(ActiveFacts::API::DuplicateIdentifyingValueException)
436
440
  end
437
441
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activefacts-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clifford Heath
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-08-12 00:00:00.000000000 Z
11
+ date: 2014-08-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rbtree-pure