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