activefacts-api 1.1.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/VERSION +1 -1
- data/activefacts-api.gemspec +3 -3
- data/lib/activefacts/api/constellation.rb +64 -57
- data/lib/activefacts/api/entity.rb +28 -28
- data/lib/activefacts/api/exceptions.rb +1 -1
- data/lib/activefacts/api/guid.rb +25 -6
- data/lib/activefacts/api/instance.rb +21 -1
- data/lib/activefacts/api/object_type.rb +62 -23
- data/lib/activefacts/api/role_values.rb +18 -7
- data/spec/constellation/instance_spec.rb +4 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
NjExZjgzZWEyZGI1Njg0ZGE5Mzc1NmU1M2RhNWRmYTQwY2Y5ODAxMg==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
YWUzNGNkMmEyOWQ4YWNjNDI0OWUyOTZkZTJhYzBhYTdmNGYxMmRkOA==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
MDYwZmQ4ZDU4ZGNiMDZmZjNlYTUzZDc4M2JlMThlM2Y0YzMzNWRmMjdiZGYw
|
10
|
+
ZDJjM2U5ZTY4NDMzNjNkYzE3Njk0NDA4ZTMwMTZjYjI3Y2JkNzM3N2M5NTVk
|
11
|
+
NThlMjM4MjE5ZDA2YzljMmJkYTk0OTdmNDMxMWRjODM5Y2IwMTA=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
NDdjZmU0ZDVlNzJkY2Y5OTI3ZGFiYzNkNWUxY2EyNjVjYTJkODdhZDBmYWE2
|
14
|
+
ZDY3N2E1YTQzM2E4NDk0MjY2N2YwNmFlNjk3MmVjZTgxZjg0MDJhN2Y4Y2Jh
|
15
|
+
ZjFiMjEwZjNjMjc1NmI5ODI2MDJhYmM0M2I4MWRkZDdlMGZlZjY=
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.3.0
|
data/activefacts-api.gemspec
CHANGED
@@ -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.
|
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.
|
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-
|
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
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
48
|
-
|
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
|
-
|
57
|
+
rescue NoMethodError => e
|
58
|
+
raise settable_roles_exception(e, role_name)
|
55
59
|
end
|
56
|
-
|
60
|
+
end
|
57
61
|
end
|
58
62
|
|
59
|
-
|
60
|
-
#
|
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
|
|
data/lib/activefacts/api/guid.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
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
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
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 :
|
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
|
16
|
-
@
|
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 =
|
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|
|
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
|
-
|
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.
|
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-
|
11
|
+
date: 2014-08-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rbtree-pure
|