activefacts-api 0.9.3 → 0.9.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|