activefacts-api 0.9.3 → 0.9.4
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.
- data/TODO +16 -0
- data/VERSION +1 -1
- data/activefacts-api.gemspec +4 -2
- data/lib/activefacts/api/constellation.rb +118 -50
- data/lib/activefacts/api/date.rb +98 -0
- data/lib/activefacts/api/entity.rb +198 -206
- data/lib/activefacts/api/exceptions.rb +19 -4
- data/lib/activefacts/api/guid.rb +4 -13
- data/lib/activefacts/api/instance.rb +55 -93
- data/lib/activefacts/api/instance_index.rb +1 -32
- data/lib/activefacts/api/numeric.rb +51 -55
- data/lib/activefacts/api/object_type.rb +155 -151
- data/lib/activefacts/api/role.rb +3 -32
- data/lib/activefacts/api/standard_types.rb +8 -4
- data/lib/activefacts/api/support.rb +0 -22
- data/lib/activefacts/api/value.rb +62 -39
- data/lib/activefacts/tracer.rb +8 -6
- data/spec/constellation/constellation_spec.rb +150 -80
- data/spec/constellation/instance_spec.rb +97 -73
- data/spec/fact_type/role_values_spec.rb +33 -12
- data/spec/fact_type/roles_spec.rb +4 -28
- data/spec/identification_scheme/identification_spec.rb +1 -0
- data/spec/identification_scheme/identity_change_spec.rb +4 -4
- data/spec/metadata_spec.rb +269 -0
- data/spec/object_type/entity_type/multipart_identification_spec.rb +1 -2
- data/spec/object_type/value_type/autocounter_spec.rb +4 -4
- data/spec/object_type/value_type/date_time_spec.rb +1 -1
- data/spec/object_type/value_type/guid_spec.rb +3 -3
- data/spec/object_type/value_type/value_type_spec.rb +2 -1
- data/spec/simplecov_helper.rb +3 -2
- metadata +5 -3
@@ -18,8 +18,8 @@ module ActiveFacts
|
|
18
18
|
|
19
19
|
# Each ObjectType maintains a list of the Roles it plays:
|
20
20
|
def roles(role_name = nil)
|
21
|
-
unless instance_variable_defined? "@roles"
|
22
|
-
@roles = RoleCollection.new
|
21
|
+
unless instance_variable_defined? "@roles" # Avoid "instance variable not defined" warning from ||=
|
22
|
+
@roles = RoleCollection.new
|
23
23
|
end
|
24
24
|
case role_name
|
25
25
|
when nil
|
@@ -65,7 +65,7 @@ module ActiveFacts
|
|
65
65
|
# * :restrict - a list of values or ranges which this role may take. Not used yet.
|
66
66
|
def has_one(role_name, options = {})
|
67
67
|
role_name, related, mandatory, related_role_name = extract_binary_params(false, role_name, options)
|
68
|
-
|
68
|
+
check_identifying_role_has_valid_cardinality(:has_one, role_name)
|
69
69
|
define_binary_fact_type(false, role_name, related, mandatory, related_role_name)
|
70
70
|
end
|
71
71
|
|
@@ -82,20 +82,20 @@ module ActiveFacts
|
|
82
82
|
def one_to_one(role_name, options = {})
|
83
83
|
role_name, related, mandatory, related_role_name =
|
84
84
|
extract_binary_params(true, role_name, options)
|
85
|
-
|
85
|
+
check_identifying_role_has_valid_cardinality(:one_to_one, role_name)
|
86
86
|
define_binary_fact_type(true, role_name, related, mandatory, related_role_name)
|
87
87
|
end
|
88
88
|
|
89
|
-
def
|
90
|
-
if
|
91
|
-
case
|
89
|
+
def check_identifying_role_has_valid_cardinality(type, role)
|
90
|
+
if is_entity_type && identifying_role_names.include?(role)
|
91
|
+
case type
|
92
92
|
when :has_one
|
93
93
|
if identifying_role_names.size == 1
|
94
|
-
raise "Entity type #{self} cannot be identified by a single role '#{
|
94
|
+
raise "Entity type #{self} cannot be identified by a single role '#{role}' unless that role is one_to_one"
|
95
95
|
end
|
96
96
|
when :one_to_one
|
97
97
|
if identifying_role_names.size > 1
|
98
|
-
raise "Entity type #{self} cannot be identified by a single role '#{
|
98
|
+
raise "Entity type #{self} cannot be identified by a single role '#{role}' unless that role is has_one"
|
99
99
|
end
|
100
100
|
end
|
101
101
|
end
|
@@ -108,51 +108,47 @@ module ActiveFacts
|
|
108
108
|
# Without parameters, it returns the array of ObjectType supertypes
|
109
109
|
# (one by Ruby inheritance, any others as defined using this method)
|
110
110
|
def supertypes(*object_types)
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
[(superclass.respond_to?(:vocabulary) ? superclass : nil), *@supertypes].compact
|
140
|
-
end
|
111
|
+
@supertypes ||= []
|
112
|
+
all_supertypes = supertypes_transitive
|
113
|
+
object_types.each do |object_type|
|
114
|
+
next if all_supertypes.include? object_type
|
115
|
+
supertype =
|
116
|
+
case object_type
|
117
|
+
when Class
|
118
|
+
object_type
|
119
|
+
when Symbol
|
120
|
+
# No late binding here:
|
121
|
+
(object_type = vocabulary.const_get(object_type.to_s.camelcase))
|
122
|
+
else
|
123
|
+
raise "Illegal supertype #{object_type.inspect} for #{self.class.basename}"
|
124
|
+
end
|
125
|
+
raise "#{supertype.name} must be an object type in #{vocabulary.name}" unless supertype.respond_to?(:vocabulary) and supertype.vocabulary == self.vocabulary
|
126
|
+
|
127
|
+
if is_entity_type != supertype.is_entity_type
|
128
|
+
raise "#{self} < #{supertype}: A value type may not be a supertype of an entity type, and vice versa"
|
129
|
+
end
|
130
|
+
|
131
|
+
@supertypes << supertype
|
132
|
+
|
133
|
+
# Realise the roles (create accessors) of this supertype.
|
134
|
+
# REVISIT: The existing accessors at the other end will need to allow this class as role counterpart
|
135
|
+
# REVISIT: Need to check all superclass roles recursively, unless we hit a common supertype
|
136
|
+
realise_supertypes(object_type, all_supertypes)
|
137
|
+
end
|
138
|
+
[(superclass.respond_to?(:vocabulary) ? superclass : nil), *@supertypes].compact
|
141
139
|
end
|
142
140
|
|
143
141
|
# Return the array of all ObjectType supertypes, transitively.
|
144
142
|
def supertypes_transitive
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
sts # The local variable unconfuses rcov
|
155
|
-
end
|
143
|
+
supertypes = []
|
144
|
+
v = superclass.respond_to?(:vocabulary) ? superclass.vocabulary : nil
|
145
|
+
supertypes << superclass if v.kind_of?(Module)
|
146
|
+
supertypes += (@supertypes ||= [])
|
147
|
+
sts = supertypes.inject([]) do |a, t|
|
148
|
+
next if a.include?(t)
|
149
|
+
a += [t] + t.supertypes_transitive
|
150
|
+
end.uniq
|
151
|
+
sts # The local variable unconfuses rcov
|
156
152
|
end
|
157
153
|
|
158
154
|
def subtypes
|
@@ -175,10 +171,6 @@ module ActiveFacts
|
|
175
171
|
end
|
176
172
|
end
|
177
173
|
|
178
|
-
def is_a? klass
|
179
|
-
super || supertypes_transitive.include?(klass)
|
180
|
-
end
|
181
|
-
|
182
174
|
private
|
183
175
|
|
184
176
|
def realise_supertypes(object_type, all_supertypes = nil)
|
@@ -220,118 +212,130 @@ module ActiveFacts
|
|
220
212
|
end
|
221
213
|
|
222
214
|
def define_unary_role_accessor(role)
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
end
|
235
|
-
end
|
215
|
+
define_method role.setter do |value|
|
216
|
+
assigned = case value
|
217
|
+
when nil; nil
|
218
|
+
when false; false
|
219
|
+
else true
|
220
|
+
end
|
221
|
+
instance_variable_set(role.variable, assigned)
|
222
|
+
# REVISIT: Provide a way to find all instances playing/not playing this role
|
223
|
+
# Analogous to true.all_thing_as_role_name...
|
224
|
+
assigned
|
225
|
+
end
|
236
226
|
define_single_role_getter(role)
|
237
227
|
end
|
238
228
|
|
239
229
|
def define_single_role_getter(role)
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
end
|
245
|
-
end
|
230
|
+
define_method role.getter do |*a|
|
231
|
+
raise "Parameters passed to #{self.class.name}\##{role.name}" if a.size > 0
|
232
|
+
instance_variable_get(role.variable)
|
233
|
+
end
|
246
234
|
end
|
247
235
|
|
248
236
|
def define_one_to_one_accessor(role)
|
249
237
|
define_single_role_getter(role)
|
250
238
|
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
239
|
+
define_method role.setter do |value|
|
240
|
+
old = instance_variable_get(role.variable)
|
241
|
+
|
242
|
+
# When exactly the same value instance is assigned, we're done:
|
243
|
+
return true if old == value
|
244
|
+
|
245
|
+
if value and o = role.counterpart.object_type and (!value.is_a?(o) || value.constellation != @constellation)
|
246
|
+
value = @constellation.assert(o, *Array(value))
|
247
|
+
# return true if old.equal?(value) # Occurs when same value but not same instance is assigned
|
248
|
+
return true if old == value
|
249
|
+
end
|
250
|
+
|
251
|
+
dependent_entities = nil
|
252
|
+
if (role.is_identifying)
|
253
|
+
check_value_change_legality(role, value)
|
254
|
+
|
255
|
+
# We're changing this object's key.
|
256
|
+
# Find all object which are identified by this object, and save their old keys
|
257
|
+
if @constellation && old
|
258
|
+
dependent_entities = old.related_entities.map do |entity|
|
259
|
+
[entity.identifying_role_values, entity]
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
instance_variable_set(role.variable, value)
|
265
|
+
|
266
|
+
# Remove self from the old counterpart:
|
267
|
+
old.send(role.counterpart.setter, nil) if old
|
268
|
+
|
269
|
+
@constellation.when_admitted do
|
270
|
+
# Assign self to the new counterpart
|
271
|
+
value.send(role.counterpart.setter, self) if value
|
272
|
+
|
273
|
+
# Propagate dependent key changes
|
274
|
+
if dependent_entities
|
275
|
+
dependent_entities.each do |old_key, entity|
|
276
|
+
entity.instance_index.refresh_key(old_key)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
value
|
282
|
+
end
|
285
283
|
end
|
286
284
|
|
287
285
|
def define_one_to_many_accessor(role)
|
288
286
|
define_single_role_getter(role)
|
289
287
|
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
288
|
+
define_method role.setter do |value|
|
289
|
+
role_var = role.variable
|
290
|
+
|
291
|
+
# Get old value, and jump out early if it's unchanged:
|
292
|
+
old = instance_variable_get(role_var)
|
293
|
+
return value if old.equal?(value) # Occurs during one_to_one assignment, for example
|
294
|
+
|
295
|
+
# assert a new instance for the role value if necessary
|
296
|
+
if value and o = role.counterpart.object_type and (!value.is_a?(o) || value.constellation != @constellation)
|
297
|
+
value = @constellation.assert(o, *Array(value))
|
298
|
+
return value if old.equal?(value) # Occurs when another instance having the same value is assigned
|
299
|
+
end
|
300
|
+
|
301
|
+
dependent_entities = nil
|
302
|
+
if (role.is_identifying)
|
303
|
+
check_value_change_legality(role, value) if value
|
304
|
+
|
305
|
+
if old && old.constellation
|
306
|
+
# If our identity has changed and we identify others, prepare to reindex them
|
307
|
+
dependent_entities = old.related_entities.map do |entity|
|
308
|
+
[entity.identifying_role_values, entity]
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
instance_variable_set(role_var, value)
|
314
|
+
|
315
|
+
# Remove "self" from the old counterpart:
|
316
|
+
old.send(getter = role.counterpart.getter).update(self, nil) if old
|
317
|
+
|
318
|
+
@constellation.when_admitted do
|
319
|
+
# REVISIT: Delay co-referencing here if the object is still a candidate
|
320
|
+
# Add "self" into the counterpart
|
321
|
+
value.send(getter ||= role.counterpart.getter).update(old, self) if value
|
322
|
+
|
323
|
+
if dependent_entities
|
324
|
+
dependent_entities.each do |key, entity|
|
325
|
+
entity.instance_index.refresh_key(key)
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
value
|
331
|
+
end
|
326
332
|
end
|
327
333
|
|
328
334
|
def define_many_to_one_accessor(role)
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
instance_variable_set(role_var, RoleValues.new)
|
334
|
-
end
|
335
|
+
define_method role.getter do
|
336
|
+
role_var = role.variable
|
337
|
+
instance_variable_get(role_var) or
|
338
|
+
instance_variable_set(role_var, RoleValues.new)
|
335
339
|
end
|
336
340
|
end
|
337
341
|
|
@@ -375,7 +379,7 @@ module ActiveFacts
|
|
375
379
|
role_name = (Class === role_name ? a.name.snakecase : role_name).to_sym
|
376
380
|
|
377
381
|
# The related class might be forward-referenced, so handle a Symbol/String instead of a Class.
|
378
|
-
related_name = options.delete(:class)
|
382
|
+
specified_class = related_name = options.delete(:class)
|
379
383
|
case related_name
|
380
384
|
when nil
|
381
385
|
related = role_name # No :class provided, assume it matches the role_name
|
@@ -412,7 +416,7 @@ module ActiveFacts
|
|
412
416
|
|
413
417
|
# Avoid a confusing mismatch:
|
414
418
|
# Note that if you have a role "supervisor" and a sub-class "Supervisor", this'll bitch.
|
415
|
-
if (Class === related && (indicated = vocabulary.object_type(role_name)) && indicated != related)
|
419
|
+
if (!specified_class && Class === related && (indicated = vocabulary.object_type(role_name)) && indicated != related)
|
416
420
|
raise "Role name #{role_name} indicates a different counterpart object_type #{indicated} than specified"
|
417
421
|
end
|
418
422
|
|
data/lib/activefacts/api/role.rb
CHANGED
@@ -55,42 +55,13 @@ module ActiveFacts
|
|
55
55
|
counterpart == nil
|
56
56
|
end
|
57
57
|
|
58
|
-
def is_inherited?(klass)
|
59
|
-
klass.supertypes_transitive.include?(@object_type)
|
60
|
-
end
|
61
|
-
|
62
|
-
def counterpart_object_type
|
63
|
-
# This method is sometimes used when unaries are used in an entity's identifier.
|
64
|
-
@is_unary ? TrueClass : (counterpart ? counterpart.object_type : nil)
|
65
|
-
end
|
66
|
-
|
67
58
|
def inspect
|
68
59
|
"<Role #{object_type.name}.#{name}>"
|
69
60
|
end
|
70
61
|
|
71
|
-
def
|
72
|
-
|
73
|
-
|
74
|
-
if value.is_a?(counterpart.object_type)
|
75
|
-
# Check that the value is in a compatible constellation, clone if not:
|
76
|
-
if constellation && (vc = value.constellation) && vc != constellation
|
77
|
-
# Cross-constellation assignment!
|
78
|
-
# Just take the identifying_role_values to make a new object
|
79
|
-
value = constellation.send(value.class.basename, value.identifying_role_values)
|
80
|
-
end
|
81
|
-
value.constellation = constellation if constellation
|
82
|
-
else
|
83
|
-
value = [value] unless Array === value
|
84
|
-
raise "No parameters were provided to identify an #{counterpart.object_type.basename} instance" if value == []
|
85
|
-
if constellation
|
86
|
-
value = constellation.send(counterpart.object_type.basename.to_sym, *value)
|
87
|
-
else
|
88
|
-
#trace :assert, "Constructing new #{counterpart.object_type} with #{value.inspect}" do
|
89
|
-
value = counterpart.object_type.new(*value)
|
90
|
-
#end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
value
|
62
|
+
def verbalise
|
63
|
+
"Role #{name} of #{object_type}, " +
|
64
|
+
(@is_unary ? 'unary' : (counterpart ? 'played by' + counterpart.object_type : 'undefined'))
|
94
65
|
end
|
95
66
|
|
96
67
|
private
|
@@ -7,8 +7,8 @@
|
|
7
7
|
# These extensions add ActiveFacts ObjectType and Instance behaviour into base Ruby value classes,
|
8
8
|
# and allow any Class to become an Entity.
|
9
9
|
#
|
10
|
-
require 'date'
|
11
10
|
require 'activefacts/api/numeric'
|
11
|
+
require 'activefacts/api/date'
|
12
12
|
require 'activefacts/api/guid'
|
13
13
|
|
14
14
|
module ActiveFacts
|
@@ -16,6 +16,7 @@ module ActiveFacts
|
|
16
16
|
# Adapter module to add value_type to all potential value classes
|
17
17
|
module ValueClass #:nodoc:
|
18
18
|
def value_type *args, &block #:nodoc:
|
19
|
+
# The inclusion of instance methods triggers ClassMethods to be included in the class too
|
19
20
|
include ActiveFacts::API::Value
|
20
21
|
value_type(*args, &block)
|
21
22
|
end
|
@@ -29,22 +30,23 @@ ValueClasses.each{|c|
|
|
29
30
|
c.send :extend, ActiveFacts::API::ValueClass
|
30
31
|
}
|
31
32
|
|
33
|
+
# Cannot subclass or delegate True, False or nil, so inject the required behaviour
|
32
34
|
class TrueClass #:nodoc:
|
33
35
|
def verbalise(role_name = nil); role_name ? "#{role_name}: true" : "true"; end
|
34
36
|
def identifying_role_values; self; end
|
35
|
-
def self.identifying_role_values(*a);
|
37
|
+
def self.identifying_role_values(*a); a.replace([{}]); true end
|
36
38
|
end
|
37
39
|
|
38
40
|
class FalseClass #:nodoc:
|
39
41
|
def verbalise(role_name = nil); role_name ? "#{role_name}: false" : "false"; end
|
40
42
|
def identifying_role_values; self; end
|
41
|
-
def self.identifying_role_values(*a);
|
43
|
+
def self.identifying_role_values(*a); a.replace([{}]); false end
|
42
44
|
end
|
43
45
|
|
44
46
|
class NilClass #:nodoc:
|
45
47
|
def verbalise; "nil"; end
|
46
48
|
def identifying_role_values; self; end
|
47
|
-
def self.identifying_role_values(*a);
|
49
|
+
def self.identifying_role_values(*a); a.replace([{}]); nil end
|
48
50
|
end
|
49
51
|
|
50
52
|
class Class
|
@@ -52,6 +54,8 @@ class Class
|
|
52
54
|
# The parameters are the names (Symbols) of the identifying roles.
|
53
55
|
def identified_by *args, &b
|
54
56
|
raise "#{basename} is not an entity type" if respond_to? :value_type # Don't make a ValueType into an EntityType
|
57
|
+
|
58
|
+
# The inclusion of instance methods triggers ClassMethods to be included in the class too
|
55
59
|
include ActiveFacts::API::Entity
|
56
60
|
identified_by(*args, &b)
|
57
61
|
end
|