activefacts-api 1.0.0 → 1.1.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
- MDI2NTVhZWJlYmU5NDhlNmFlZTg5ZjBkNzY2YTMxMDI2MjJiYWRlZA==
4
+ ZjdhYmJiODJmYjRmM2IwYjE5MDUyOWEzYTk2ZTlhZTNiOTlkYTZjMg==
5
5
  data.tar.gz: !binary |-
6
- NzJjODlmMmNjZjgxMmIxNzFkZmJkZjNmMGYyZDNkOGZkMTIxNGRhYg==
6
+ MGE3OGQ5OWY4MThhMzUyYzE3NmU3NDJkMWMwZWJhNWQyMjE1OTVkZQ==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- MGU5M2Y2ZWM1YjgwN2ZjMWM2MjBlOGVhYzAyOGRhNjBjZWY2YTM5ZmNjZWQz
10
- ZTJlYTY2YTQ2YzE3YjA2ZDA2NWVmZmM4YmRiMmEwZDRiNmU2NDBlYzQ3ODc2
11
- YjhjOTNmMTA1MDhiZWFiOGRkMjc3YzE1NjIxMGUzOTEzMTYxYTE=
9
+ M2FiZDNkMzA5YTcxOTQyOGNmODBhZDI4YjgwNTBmOTM1NmViMDhiMjcxNDhk
10
+ Mjg0Njg4NGM3OWQ0YjIzN2IyOWU0MzAwYzMzMmU1N2IxMDg1YjU2ZTdmMDNk
11
+ N2ViMjQyMmYwYmM1YzhlZjNlZGUxZTI5OTNmNjAzNzhhYmY3MGU=
12
12
  data.tar.gz: !binary |-
13
- OWFkY2JiMTQwZTBkYjlhNGFhNGNiOWIxMWZmODEzZjcyYmNhZjNkZWM0N2U4
14
- M2ZhMGNlYTBjNTAxNzdiZDAwNzc5ZDY1ZWNhYTY3MTNlNDBhMGQwZjFiNGZh
15
- MTc2NGMxN2VmYmZjYTEzOWYxODdmMjM4NDkyNzRkMmI4MDUzN2Q=
13
+ YTRiY2IyMTgzMmQwYTU5ODhjZTcwNDE4MDUzMjVhYjllNTAxOTc1ZjFhNzFm
14
+ NjhhNmRiMGEzZGUwYjgxNTI4OWZhODQ4MGRiYmQ5NTMzNDUxOGY4NDY4ODVj
15
+ ZGE4NWM3NzY0Njc1MDc0NmMzYmM3NDNkOTdjNzVmOTMxNDI4ODU=
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0
1
+ 1.1.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.0.0 ruby lib
5
+ # stub: activefacts-api 1.1.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "activefacts-api"
9
- s.version = "1.0.0"
9
+ s.version = "1.1.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-02-18"
14
+ s.date = "2014-08-12"
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 = [
@@ -145,7 +145,9 @@ module ActiveFacts
145
145
  last_irvs = instance.identifying_role_values(klass)
146
146
  last_irns = n
147
147
  end
148
- instances[klass].delete(last_irvs)
148
+ deleted = instances[klass].delete(last_irvs)
149
+ # The RBTree class sometimes returns a different object than what was deleted! Check non-nil:
150
+ raise "Internal error: deindex #{instance.class} as #{klass} failed" if deleted == nil
149
151
  end
150
152
  end
151
153
 
@@ -188,10 +190,15 @@ module ActiveFacts
188
190
  klass = vocabulary.const_get(object_type)
189
191
 
190
192
  single_roles, multiple_roles = klass.all_role.
191
- partition{|n, r| r.unique }.
192
- map{ |rs| rs.map{|n, r| n}.sort_by(&:to_s) }
193
-
194
- single_roles -= klass.identifying_role_names if (klass.is_entity_type)
193
+ partition do |n, r|
194
+ r.unique && # Show only single-valued roles
195
+ !r.is_identifying && # Unless identifying
196
+ (r.unary? || !r.counterpart.is_identifying) # Or identifies a counterpart
197
+ end.
198
+ map do |rs|
199
+ rs.map{|n, r| n}.
200
+ sort_by(&:to_s)
201
+ end
195
202
 
196
203
  instances = send(object_type.to_sym)
197
204
  next nil unless instances.size > 0
@@ -222,6 +229,87 @@ module ActiveFacts
222
229
  end.compact*"\n"
223
230
  end
224
231
 
232
+ def clone
233
+ remaining_object_types = vocabulary.object_type.clone
234
+ constellation = self.class.new(vocabulary, @options)
235
+ correlates = {}
236
+ other_attribute_assignments = []
237
+ until remaining_object_types.empty?
238
+ count = 0
239
+ # Choose an object type we can clone now:
240
+ name, object_type = *remaining_object_types.detect do |name, o|
241
+ (count = @instances[o].size) == 0 or # There are no instances of this object type; clone is ok
242
+ !o.is_entity_type or # It's a value type
243
+ (
244
+ !o.subtypes_transitive.detect do |subtype|# All its subtypes have been cloned
245
+ remaining_object_types.has_key?(subtype.basename)
246
+ end and
247
+ !o.identifying_roles.detect do |role| # The players of its identifying roles have all been dumped
248
+ next unless role.counterpart # Unary role, no player
249
+ counterpart = role.counterpart.object_type # counterpart object
250
+
251
+ # The identifying type and its subtypes have been dumped
252
+ ([counterpart]+counterpart.subtypes_transitive).detect do |subtype|
253
+ remaining_object_types.has_key?(subtype.basename)
254
+ end
255
+ end
256
+ )
257
+ end
258
+ # puts "Cloning #{count} instances of #{name}" if count > 0
259
+ remaining_object_types.delete(name)
260
+
261
+ key_role_names =
262
+ if object_type.is_entity_type
263
+ ([object_type]+object_type.supertypes_transitive).map { |t| t.identifying_role_names }.flatten.uniq
264
+ else
265
+ nil
266
+ end
267
+ other_roles = object_type.all_role_transitive.map do |role_name, role|
268
+ next if !role.unique or
269
+ role.fact_type.class == ActiveFacts::API::TypeInheritanceFactType or
270
+ role.fact_type.all_role[0] != role or # Only the first role in a one-to-one pair
271
+ key_role_names.include?(role_name)
272
+ role
273
+ end.compact - Array(key_role_names)
274
+
275
+ @instances[object_type].each do |key, object|
276
+ next if object.class != object_type
277
+
278
+ # Clone this object
279
+
280
+ # Get the identifying values:
281
+ key = object
282
+ if (key_role_names)
283
+ key = key_role_names.inject({}) do |h, krn|
284
+ h[krn] = object.send(krn)
285
+ h
286
+ end
287
+ end
288
+ # puts "\tcloning #{object.class} #{key.inspect}"
289
+ # puts "\t\talso copy #{other_roles.map(&:name)*', '}"
290
+
291
+ new_object = constellation.assert(object_type, key)
292
+ correlates[object] = new_object
293
+
294
+ other_roles.each do |role|
295
+ value = object.send(role.getter)
296
+ next unless value
297
+ other_attribute_assignments << proc do
298
+ new_object.send(role.setter, correlates[value])
299
+ end
300
+ end
301
+ end
302
+ end
303
+
304
+ # Now, assign all non-identifying facts
305
+ # puts "Assigning #{other_attribute_assignments.size} additional roles"
306
+ other_attribute_assignments.each do |assignment|
307
+ assignment.call
308
+ end
309
+
310
+ constellation
311
+ end
312
+
225
313
  end
226
314
 
227
315
  def self.sorted
@@ -204,6 +204,30 @@ module ActiveFacts
204
204
  end
205
205
  end
206
206
 
207
+ # If this instance's role is updated to the new value, does that cause a collision?
208
+ # We need to check each superclass that has a different identification pattern
209
+ def check_identification_change_legality(role, value)
210
+ return unless @constellation && role.is_identifying
211
+ return if @constellation.send(:instance_variable_get, :@suspend_duplicate_key_check)
212
+
213
+ klasses = [self.class] + self.class.supertypes_transitive
214
+ last_identity = nil
215
+ last_irns = nil
216
+ counterpart_class = role.counterpart ? role.counterpart.object_type : value.class
217
+ duplicate = klasses.detect do |klass|
218
+ next false unless klass.identifying_roles.include?(role)
219
+ irns = klass.identifying_role_names
220
+ if last_irns != irns
221
+ last_identity = identifying_role_values(klass)
222
+ role_position = irns.index(role.name)
223
+ last_identity[role_position] = value.identifying_role_values(counterpart_class)
224
+ end
225
+ @constellation.instances[klass][last_identity]
226
+ end
227
+
228
+ raise DuplicateIdentifyingValueException.new(self.class, role.name, value) if duplicate
229
+ end
230
+
207
231
  # All classes that become Entity types receive the methods of this class as class methods:
208
232
  module ClassMethods
209
233
  include Instance::ClassMethods
@@ -30,30 +30,6 @@ module ActiveFacts
30
30
  super || self.class.supertypes_transitive.include?(klass)
31
31
  end
32
32
 
33
- # If this instance's role is updated to the new value, does that cause a collision?
34
- # We need to check each superclass that has a different identification pattern
35
- def check_identification_change_legality(role, value)
36
- return unless @constellation && role.is_identifying
37
- return if @constellation.send(:instance_variable_get, :@suspend_duplicate_key_check)
38
-
39
- klasses = [self.class] + self.class.supertypes_transitive
40
- last_identity = nil
41
- last_irns = nil
42
- counterpart_class = role.counterpart ? role.counterpart.object_type : value.class
43
- duplicate = klasses.detect do |klass|
44
- next false unless klass.identifying_roles.include?(role)
45
- irns = klass.identifying_role_names
46
- if last_irns != irns
47
- last_identity = identifying_role_values(klass)
48
- role_position = irns.index(role.name)
49
- last_identity[role_position] = value.identifying_role_values(counterpart_class)
50
- end
51
- @constellation.instances[klass][last_identity]
52
- end
53
-
54
- raise DuplicateIdentifyingValueException.new(self.class, role.name, value) if duplicate
55
- end
56
-
57
33
  # List entities which have an identifying role played by this object.
58
34
  def related_entities(indirectly = true, instances = [])
59
35
  # Check all roles of this instance
@@ -78,6 +54,7 @@ module ActiveFacts
78
54
  def retract
79
55
  # Delete from the constellation first, while we remember our identifying role values
80
56
  @constellation.deindex_instance(self) if @constellation
57
+ instance_variable_set(@@constellation_variable_name ||= "@constellation", nil)
81
58
 
82
59
  # Now, for all roles (from this class and all supertypes), assign nil to all functional roles
83
60
  # The counterpart roles get cleared automatically.
@@ -93,6 +70,26 @@ module ActiveFacts
93
70
  irvrvs[role_values] = role_values.index_values(self)
94
71
  end
95
72
 
73
+ # Nullify the counterpart role of objects we identify first, before damaging our identifying_role_values:
74
+ klasses.each do |klass|
75
+ klass.all_role.each do |role_name, role|
76
+ next if role.unary?
77
+ next if !(counterpart = role.counterpart).is_identifying
78
+ next if role.fact_type.is_a?(TypeInheritanceFactType)
79
+
80
+ counterpart_instances = send(role.getter)
81
+ counterpart_instances.to_a.each do |counterpart_instance|
82
+ # Allow nullifying non-mandatory roles, as long as they're not identifying.
83
+ if counterpart.mandatory
84
+ counterpart_instance.retract
85
+ else
86
+ counterpart_instance.send(counterpart.setter, nil, false)
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ # Now deal with other roles:
96
93
  klasses.each do |klass|
97
94
  klass.all_role.each do |role_name, role|
98
95
  next if role.unary?
@@ -104,31 +101,28 @@ module ActiveFacts
104
101
  next if role.fact_type.is_a?(TypeInheritanceFactType)
105
102
  i = send(role.getter)
106
103
  next unless i
107
- if counterpart.is_identifying && counterpart.mandatory
108
- # We play a mandatory identifying role in i; so retract that (it'll clear our instance variable)
109
- i.retract
104
+
105
+ if (counterpart.unique)
106
+ # REVISIT: This will incorrectly fail to propagate a key change for a non-mandatory role
107
+ i.send(counterpart.setter, nil, false)
110
108
  else
111
- if (counterpart.unique)
112
- # REVISIT: This will incorrectly fail to propagate a key change for a non-mandatory role
113
- i.send(counterpart.setter, nil, false)
114
- else
115
- rv = i.send(role.counterpart.getter)
116
- rv.delete_instance(self, irvrvs[rv])
109
+ rv = i.send(role.counterpart.getter)
110
+ rv.delete_instance(self, irvrvs[rv])
111
+
112
+ if (rv.empty? && !i.class.is_entity_type)
113
+ i.retract if i.plays_no_role
117
114
  end
115
+
118
116
  end
119
117
  instance_variable_set(role.variable, nil)
120
118
  else
121
119
  # puts "Not removing role #{role_name} from counterpart RoleValues #{counterpart.name}"
122
120
  # Duplicate the array using to_a, as the RoleValues here will be modified as we traverse it:
123
121
  next if role.fact_type.is_a?(TypeInheritanceFactType)
124
- counterpart_instances = send(role.name)
122
+ counterpart_instances = send(role.getter)
125
123
  counterpart_instances.to_a.each do |counterpart_instance|
126
- # These actions deconstruct the RoleValues as we go:
127
- if counterpart.is_identifying && counterpart.mandatory
128
- counterpart_instance.retract
129
- else
130
- counterpart_instance.send(counterpart.setter, nil, false)
131
- end
124
+ # This action deconstructs our RoleValues as we go:
125
+ counterpart_instance.send(counterpart.setter, nil, false)
132
126
  end
133
127
  instance_variable_set(role.variable, nil)
134
128
  end
@@ -172,6 +172,10 @@ module ActiveFacts
172
172
  @subtypes ||= []
173
173
  end
174
174
 
175
+ def subtypes_transitive
176
+ (subtypes+subtypes.map(&:subtypes_transitive)).flatten.uniq
177
+ end
178
+
175
179
  # Every new role added or inherited comes through here:
176
180
  def realise_role(role) #:nodoc:
177
181
  if (role.unary?)
@@ -328,7 +332,12 @@ module ActiveFacts
328
332
  instance_variable_set(role_var, value)
329
333
 
330
334
  # Remove "self" from the old counterpart:
331
- old_role_values.delete_instance(self, old_key) if old_key
335
+ if old_key
336
+ old_role_values.delete_instance(self, old_key)
337
+ if (old_role_values.empty? && !old.class.is_entity_type)
338
+ old.retract if old.plays_no_role
339
+ end
340
+ end
332
341
 
333
342
  @constellation.when_admitted do
334
343
  # Add "self" into the counterpart
@@ -32,6 +32,20 @@ module ActiveFacts
32
32
  respond_to?(:__getobj__) ? __getobj__ : self
33
33
  end
34
34
 
35
+ def plays_no_role
36
+ # REVISIT: Some Value Types are independent, and so must always be regarded as playing a role
37
+ self.class.all_role.all? do |n, role|
38
+ case
39
+ when role.fact_type.is_a?(ActiveFacts::API::TypeInheritanceFactType)
40
+ true # No need to consider subtyping/supertyping roles here
41
+ when role.unique
42
+ send(role.getter) == nil
43
+ else
44
+ send(role.getter).empty?
45
+ end
46
+ end
47
+ end
48
+
35
49
  # All ValueType classes include the methods defined here
36
50
  module ClassMethods
37
51
  include Instance::ClassMethods
@@ -17,7 +17,7 @@ describe "In a vocabulary" do
17
17
  :add_role, :all_role, :subtypes, :supertypes, :vocabulary,
18
18
  :all_role_transitive,
19
19
  # To make private:
20
- :check_identifying_role_has_valid_cardinality, :realise_role, :supertypes_transitive,
20
+ :check_identifying_role_has_valid_cardinality, :realise_role, :supertypes_transitive, :subtypes_transitive,
21
21
  ]
22
22
 
23
23
  ValueType_methods = [
@@ -28,7 +28,7 @@ describe "In a vocabulary" do
28
28
  Instance_methods = [
29
29
  :constellation, :retract, :is_a?,
30
30
  # To remove or move to EntityType
31
- :related_entities, :check_identification_change_legality,
31
+ :related_entities,
32
32
  :instance_index
33
33
  ]
34
34
  Value_methods = Instance_methods + [
@@ -52,7 +52,7 @@ describe "In a vocabulary" do
52
52
  Entity_methods = Instance_methods + [
53
53
  :verbalise, :identifying_role_values,
54
54
  # To remove hide or rewrite:
55
- :identity_by, :identity_as_hash
55
+ :identity_by, :identity_as_hash, :check_identification_change_legality,
56
56
  ]
57
57
 
58
58
  Cases =
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.0.0
4
+ version: 1.1.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-02-18 00:00:00.000000000 Z
11
+ date: 2014-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rbtree-pure