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
data/TODO
CHANGED
@@ -1,4 +1,20 @@
|
|
1
1
|
Performance
|
2
|
+
Each object type (class) needs fast access to:
|
3
|
+
Its identifying roles
|
4
|
+
Its supertypes that have alternate identification
|
5
|
+
The roles it plays in identifying other object types
|
6
|
+
Each one-to-many role needs:
|
7
|
+
A method to derive a counterpart (RoleValues) key from a full key
|
8
|
+
Each class needs
|
9
|
+
An adapt() method to convert offered values to a full key
|
10
|
+
an assign_all method
|
11
|
+
that can perform "atomic" identity change
|
12
|
+
Constellation needs assert_instance that for a given class:
|
13
|
+
adapts all keys from identifying values
|
14
|
+
checks either non-existence or uniqueness and type of the identified object for all such keys
|
15
|
+
instantiates the object if previously non-existent
|
16
|
+
(instantiates role subtypes by mixing in if this instance did not exist but the class is mixed in another extant object)
|
17
|
+
assigns non-identifying values
|
2
18
|
Pre-define ObjectType accessor methods on constellation, rather than using method_missing
|
3
19
|
|
4
20
|
Role objects:
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.9.
|
1
|
+
0.9.4
|
data/activefacts-api.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "activefacts-api"
|
8
|
-
s.version = "0.9.
|
8
|
+
s.version = "0.9.4"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Clifford Heath"]
|
12
|
-
s.date = "
|
12
|
+
s.date = "2013-01-14"
|
13
13
|
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"
|
14
14
|
s.email = "clifford.heath@gmail.com"
|
15
15
|
s.extra_rdoc_files = [
|
@@ -29,6 +29,7 @@ Gem::Specification.new do |s|
|
|
29
29
|
"activefacts-api.gemspec",
|
30
30
|
"lib/activefacts/api.rb",
|
31
31
|
"lib/activefacts/api/constellation.rb",
|
32
|
+
"lib/activefacts/api/date.rb",
|
32
33
|
"lib/activefacts/api/entity.rb",
|
33
34
|
"lib/activefacts/api/exceptions.rb",
|
34
35
|
"lib/activefacts/api/guid.rb",
|
@@ -52,6 +53,7 @@ Gem::Specification.new do |s|
|
|
52
53
|
"spec/identification_scheme/identification_spec.rb",
|
53
54
|
"spec/identification_scheme/identity_change_spec.rb",
|
54
55
|
"spec/identification_scheme/identity_supertype_change_spec.rb",
|
56
|
+
"spec/metadata_spec.rb",
|
55
57
|
"spec/object_type/entity_type/entity_type_spec.rb",
|
56
58
|
"spec/object_type/entity_type/multipart_identification_spec.rb",
|
57
59
|
"spec/object_type/value_type/autocounter_spec.rb",
|
@@ -33,25 +33,83 @@ module ActiveFacts
|
|
33
33
|
#
|
34
34
|
class Constellation
|
35
35
|
attr_reader :vocabulary
|
36
|
-
|
36
|
+
|
37
|
+
def valid_object_type klass
|
38
|
+
klass.is_a?(Class) and klass.modspace == @vocabulary and klass.respond_to?(:assert_instance)
|
39
|
+
end
|
40
|
+
|
41
|
+
# "instances" is an index (keyed by the Class object) of indexes to instances.
|
37
42
|
# Each instance is indexed for every supertype it has (including multiply-inherited ones).
|
38
|
-
#
|
39
|
-
|
40
|
-
|
43
|
+
# The method_missing definition supports the syntax: c.MyClass.each{|k, v| ... }
|
44
|
+
def instances
|
45
|
+
@instances ||= Hash.new do |h,k|
|
46
|
+
unless valid_object_type k
|
47
|
+
raise "A constellation over #{@vocabulary.name} can only index instances of classes in that vocabulary, not #{k.inspect}"
|
48
|
+
end
|
49
|
+
h[k] = InstanceIndex.new(self, k)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Candidates is an array of object instances that do not already exist
|
54
|
+
# in the constellation but will be added if an assertion succeeds.
|
55
|
+
# After the assertion is found to be acceptable, these objects are indexed
|
56
|
+
# in the constellation and in the counterparts of their identifying roles,
|
57
|
+
# and the candidates array is nullified.
|
58
|
+
def with_candidates &b
|
59
|
+
# Multiple assignment reduces (s)teps while debugging
|
60
|
+
outermost, @candidates, @on_admission = @candidates.nil?, (@candidates || []), (@on_admission || [])
|
61
|
+
begin
|
62
|
+
b.call
|
63
|
+
rescue Exception
|
64
|
+
# Do not accept any of these candidates, there was a problem:
|
65
|
+
@candidates = [] if outermost
|
66
|
+
raise
|
67
|
+
ensure
|
68
|
+
if outermost
|
69
|
+
while @candidates
|
70
|
+
# Index the accepted instances in the constellation:
|
71
|
+
candidates = @candidates
|
72
|
+
on_admission = @on_admission
|
73
|
+
@candidates = nil
|
74
|
+
@on_admission = nil
|
75
|
+
candidates.each do |instance|
|
76
|
+
instance.class.index_instance(self, instance)
|
77
|
+
end
|
78
|
+
on_admission.each do |b|
|
79
|
+
b.call
|
80
|
+
end
|
81
|
+
# REVISIT: Admission should not create new candidates, but might start a fresh list
|
82
|
+
# debugger if @candidates and @candidates.length > 0
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def when_admitted &b
|
89
|
+
if @candidates.nil?
|
90
|
+
b.call self
|
91
|
+
else
|
92
|
+
@on_admission << b
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def candidate instance
|
97
|
+
@candidates << instance unless @candidates[-1] == instance
|
98
|
+
end
|
99
|
+
|
100
|
+
def has_candidate klass, key
|
101
|
+
@candidates && @candidates.detect{|c| c.is_a?(klass) && c.identifying_role_values == key }
|
102
|
+
end
|
41
103
|
|
42
104
|
# Create a new empty Constellation over the given Vocabulary
|
43
105
|
def initialize(vocabulary)
|
44
106
|
@vocabulary = vocabulary
|
45
|
-
@instances = Hash.new do |h,k|
|
46
|
-
unless k.is_a?(Class) and k.modspace == vocabulary
|
47
|
-
raise "A constellation over #{@vocabulary.name} can only index instances of classes in that vocabulary, not #{k.inspect}"
|
48
|
-
end
|
49
|
-
h[k] = InstanceIndex.new(self, k)
|
50
|
-
end
|
51
107
|
end
|
52
108
|
|
53
|
-
def
|
54
|
-
|
109
|
+
def assert(klass, *args)
|
110
|
+
with_candidates do
|
111
|
+
klass.assert_instance self, args
|
112
|
+
end
|
55
113
|
end
|
56
114
|
|
57
115
|
# Evaluate assertions against the population of this Constellation
|
@@ -68,6 +126,47 @@ module ActiveFacts
|
|
68
126
|
self
|
69
127
|
end
|
70
128
|
|
129
|
+
# This method removes the given instance from this constellation's indexes
|
130
|
+
# It must be called before the identifying roles get deleted or nullified.
|
131
|
+
def deindex_instance(instance) #:nodoc:
|
132
|
+
([instance.class]+instance.class.supertypes_transitive).each do |klass|
|
133
|
+
instances[klass].delete(instance.identifying_role_values(klass))
|
134
|
+
end
|
135
|
+
# REVISIT: Need to nullify all the roles this object plays.
|
136
|
+
# If mandatory on the counterpart side, this may/must propagate the delete (without mutual recursion!)
|
137
|
+
end
|
138
|
+
|
139
|
+
def define_class_accessor m, klass
|
140
|
+
(class << self; self; end).
|
141
|
+
send(:define_method, m) do |*args|
|
142
|
+
if args.size == 0
|
143
|
+
# Return the collection of all instances of this class in the constellation:
|
144
|
+
instances[klass]
|
145
|
+
else
|
146
|
+
# Assert a new ground fact (object_type instance) of the specified class, identified by args:
|
147
|
+
assert(klass, *args)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# If a missing method is the name of a class in the vocabulary module for this constellation,
|
153
|
+
# then we want to access the collection of instances of that class, and perhaps assert new ones.
|
154
|
+
# With no parameters, return the collection of all instances of that object_type.
|
155
|
+
# With parameters, assert an instance of the object_type identified by the values passed as args.
|
156
|
+
def method_missing(m, *args, &b)
|
157
|
+
klass = @vocabulary.const_get(m)
|
158
|
+
if valid_object_type klass
|
159
|
+
define_class_accessor m, klass
|
160
|
+
send(m, *args, &b)
|
161
|
+
else
|
162
|
+
super
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def inspect #:nodoc:
|
167
|
+
"Constellation:#{object_id}"
|
168
|
+
end
|
169
|
+
|
71
170
|
# Constellations verbalise all members of all classes in alphabetical order, showing
|
72
171
|
# non-identifying role values as well
|
73
172
|
def verbalise
|
@@ -88,8 +187,13 @@ module ActiveFacts
|
|
88
187
|
if (single_roles.size > 0)
|
89
188
|
role_values =
|
90
189
|
single_roles.map{|role|
|
91
|
-
|
92
|
-
|
190
|
+
value =
|
191
|
+
begin
|
192
|
+
value = instance.send(role)
|
193
|
+
rescue NoMethodError
|
194
|
+
instance.class.roles(role) # This role has not yet been realised
|
195
|
+
end
|
196
|
+
[ role_name = role.to_s.camelcase, value ]
|
93
197
|
}.select{|role_name, value|
|
94
198
|
value
|
95
199
|
}.map{|role_name, value|
|
@@ -102,42 +206,6 @@ module ActiveFacts
|
|
102
206
|
end.compact*"\n"
|
103
207
|
end
|
104
208
|
|
105
|
-
# This method removes the given instance from this constellation's indexes
|
106
|
-
# It must be called before the identifying roles get deleted or nullified.
|
107
|
-
def __retract(instance) #:nodoc:
|
108
|
-
# REVISIT: Need to search, as key values are gone already. Is there a faster way?
|
109
|
-
([instance.class]+instance.class.supertypes_transitive).each do |klass|
|
110
|
-
@instances[klass].delete_if{|k,v| v == instance }
|
111
|
-
end
|
112
|
-
# REVISIT: Need to nullify all the roles this object plays.
|
113
|
-
# If mandatory on the counterpart side, this may/must propagate the delete (without mutual recursion!)
|
114
|
-
end
|
115
|
-
|
116
|
-
# If a missing method is the name of a class in the vocabulary module for this constellation,
|
117
|
-
# then we want to access the collection of instances of that class, and perhaps assert new ones.
|
118
|
-
# With no parameters, return the collection of all instances of that object_type.
|
119
|
-
# With parameters, assert an instance of the object_type identified by the values passed as args.
|
120
|
-
def method_missing(m, *args, &b)
|
121
|
-
klass = @vocabulary.const_get(m)
|
122
|
-
if klass and klass.is_a?(Class) and klass.respond_to?(:assert_instance)
|
123
|
-
(class << self; self; end).
|
124
|
-
send(:define_method, sym = m.to_sym) do |*args|
|
125
|
-
instance_index = @instances[klass]
|
126
|
-
if args.size == 0
|
127
|
-
# Return the collection of all instances of this class in the constellation:
|
128
|
-
instance_index
|
129
|
-
else
|
130
|
-
# Assert a new ground fact (object_type instance) of the specified class, identified by args:
|
131
|
-
instance_index.assert(*args)
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
# This is the last time it'll be missing, so call it.
|
136
|
-
send(sym, *args, &b)
|
137
|
-
else
|
138
|
-
super
|
139
|
-
end
|
140
|
-
end
|
141
209
|
end
|
142
210
|
end
|
143
211
|
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Runtime API
|
3
|
+
# Date hacks to handle immediate types.
|
4
|
+
#
|
5
|
+
# Copyright (c) 2013 Clifford Heath. Read the LICENSE file.
|
6
|
+
#
|
7
|
+
# Date and DateTime don't have a sensible new() method, so we monkey-patch one here.
|
8
|
+
#
|
9
|
+
require 'date'
|
10
|
+
require 'time'
|
11
|
+
|
12
|
+
# A Date can be constructed from any Date or DateTime subclass, or parsed from a String
|
13
|
+
class ::Date
|
14
|
+
def self.new_instance constellation, *a, &b
|
15
|
+
if a[0].is_a?(String)
|
16
|
+
d = parse(*a)
|
17
|
+
elsif (a.size == 1)
|
18
|
+
case a[0]
|
19
|
+
when DateTime
|
20
|
+
d = civil(a[0].year, a[0].month, a[0].day, a[0].start)
|
21
|
+
when Date
|
22
|
+
d = civil(a[0].year, a[0].month, a[0].day, a[0].start)
|
23
|
+
when NilClass
|
24
|
+
d = civil()
|
25
|
+
else
|
26
|
+
d = civil(*a, &b)
|
27
|
+
end
|
28
|
+
else
|
29
|
+
d = civil(*a, &b)
|
30
|
+
end
|
31
|
+
d.send(:instance_variable_set, :@constellation, constellation)
|
32
|
+
d
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# A DateTime can be constructed from any Date or DateTime subclass, or parsed from a String
|
37
|
+
class ::DateTime
|
38
|
+
|
39
|
+
def self.new_instance constellation, *a, &b
|
40
|
+
if a[0].is_a?(String)
|
41
|
+
dt = parse(*a)
|
42
|
+
elsif (a.size == 1)
|
43
|
+
case a[0]
|
44
|
+
when DateTime
|
45
|
+
dt = civil(a[0].year, a[0].month, a[0].day, a[0].hour, a[0].min, a[0].sec, a[0].start)
|
46
|
+
when Date
|
47
|
+
dt = civil(a[0].year, a[0].month, a[0].day, 0, 0, 0, a[0].start)
|
48
|
+
when NilClass
|
49
|
+
dt = civil()
|
50
|
+
else
|
51
|
+
dt = civil(*a, &b)
|
52
|
+
end
|
53
|
+
else
|
54
|
+
dt = civil(*a, &b)
|
55
|
+
end
|
56
|
+
dt.send(:instance_variable_set, :@constellation, constellation)
|
57
|
+
dt
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
class ::Time
|
63
|
+
def identifying_role_values; self; end
|
64
|
+
|
65
|
+
def self.new_instance constellation, *a
|
66
|
+
t =
|
67
|
+
if a[0].is_a?(Time)
|
68
|
+
at(a[0])
|
69
|
+
else
|
70
|
+
begin
|
71
|
+
local(*a)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
=begin
|
76
|
+
if a[0].is_a?(String)
|
77
|
+
parse(*a)
|
78
|
+
elsif (a.size == 1)
|
79
|
+
case a[0]
|
80
|
+
when DateTime
|
81
|
+
a[0].clone
|
82
|
+
when Date
|
83
|
+
civil(a[0].year, a[0].month, a[0].day, 0, 0, 0, a[0].start)
|
84
|
+
when NilClass
|
85
|
+
civil()
|
86
|
+
else
|
87
|
+
civil(*a, &b)
|
88
|
+
end
|
89
|
+
else
|
90
|
+
civil(*a, &b)
|
91
|
+
end
|
92
|
+
=end
|
93
|
+
|
94
|
+
t.send(:instance_variable_set, :@constellation, constellation)
|
95
|
+
t
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
@@ -11,69 +11,61 @@ module ActiveFacts
|
|
11
11
|
module Entity
|
12
12
|
include Instance
|
13
13
|
|
14
|
-
|
15
|
-
#
|
16
|
-
# can pass bare values (array, string, integer, etc) for any role
|
17
|
-
# whose instances can be constructed using those values.
|
14
|
+
private
|
15
|
+
# Initialise a new Entity instance.
|
18
16
|
#
|
19
|
-
#
|
20
|
-
#
|
17
|
+
# arg_hash contains full-normalised and valid keys for the counterpart
|
18
|
+
# role values.
|
21
19
|
#
|
22
|
-
#
|
23
|
-
#
|
20
|
+
# This instance and its supertypes might have distinct identifiers,
|
21
|
+
# and none of the identifiers may already exist in the constellation.
|
24
22
|
#
|
25
|
-
#
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
"got (#{args.map{|a| a.to_s.inspect}*", "})" if args.size != klass.identifying_role_names.size
|
57
|
-
|
58
|
-
# Assign the identifying roles in order. Any other roles will be assigned by our caller
|
59
|
-
klass.identifying_role_names.zip(args).each do |role_name, value|
|
60
|
-
role = self.class.roles(role_name)
|
61
|
-
begin
|
62
|
-
send(role.setter, value)
|
63
|
-
rescue NoMethodError => e
|
64
|
-
raise settable_roles_exception(e, role_name)
|
65
|
-
end
|
23
|
+
# Pick out the identifying roles and assert the counterpart instances
|
24
|
+
# to assign as the new object's role values.
|
25
|
+
#
|
26
|
+
# The identifying roles of secondary supertypes must also be assigned
|
27
|
+
# here.
|
28
|
+
def initialize(arg_hash)
|
29
|
+
raise "REVISIT: Unexpected parameters in call to #{self}.new" unless arg_hash.is_a?(Hash)
|
30
|
+
|
31
|
+
super(arg_hash)
|
32
|
+
|
33
|
+
unless (klass = self.class).identification_inherited_from
|
34
|
+
irns = klass.identifying_role_names
|
35
|
+
irns.each do |role_name|
|
36
|
+
role = klass.roles(role_name)
|
37
|
+
key = arg_hash.delete(role_name)
|
38
|
+
value =
|
39
|
+
if key == nil
|
40
|
+
nil
|
41
|
+
elsif role.is_unary
|
42
|
+
(key && true) # Preserve nil and false
|
43
|
+
else
|
44
|
+
role.counterpart.object_type.assert_instance(constellation, Array(key))
|
45
|
+
end
|
46
|
+
|
47
|
+
begin
|
48
|
+
send(role.setter, value)
|
49
|
+
# rescue NoMethodError => e
|
50
|
+
# raise settable_roles_exception(e, role_name)
|
51
|
+
end
|
52
|
+
# instance_variable_set(role.setter, value)
|
53
|
+
end
|
66
54
|
end
|
67
55
|
end
|
68
56
|
|
57
|
+
=begin
|
58
|
+
# I forget how it was possible to reproduce this exception,
|
59
|
+
# so I can't get code coverage over it. It might not be still possible,
|
60
|
+
# but I can't be sure so I'll leave the code here for now.
|
69
61
|
def settable_roles_exception e, role_name
|
70
|
-
n =
|
71
|
-
"#{self.class}
|
62
|
+
n = NoMethodError.new(
|
63
|
+
"You cannot assert a #{self.class} until you define #{role_name}.\n" +
|
72
64
|
"Settable roles are #{settable_roles*', '}.\n" +
|
73
65
|
(if self.class.vocabulary.delayed.empty?
|
74
66
|
''
|
75
67
|
else
|
76
|
-
"
|
68
|
+
"Please define these object types: #{self.class.vocabulary.delayed.keys.sort*', '}\n"
|
77
69
|
end
|
78
70
|
)
|
79
71
|
)
|
@@ -92,7 +84,9 @@ module ActiveFacts
|
|
92
84
|
end.
|
93
85
|
flatten
|
94
86
|
end
|
87
|
+
=end
|
95
88
|
|
89
|
+
public
|
96
90
|
def inspect #:nodoc:
|
97
91
|
inc = constellation ? " in #{constellation.inspect}" : ""
|
98
92
|
# REVISIT: Where there are one-to-one roles, this cycles
|
@@ -133,10 +127,11 @@ module ActiveFacts
|
|
133
127
|
"#{role_name || self.class.basename}(#{ irnv*', ' })"
|
134
128
|
end
|
135
129
|
|
136
|
-
# Return the array of the values of this
|
137
|
-
def identifying_role_values
|
138
|
-
|
139
|
-
send(role_name)
|
130
|
+
# Return the array of the values of this instance's identifying roles
|
131
|
+
def identifying_role_values(klass = self.class)
|
132
|
+
klass.identifying_role_names.map do |role_name|
|
133
|
+
value = send(role_name)
|
134
|
+
value.identifying_role_values
|
140
135
|
end
|
141
136
|
end
|
142
137
|
|
@@ -193,160 +188,150 @@ module ActiveFacts
|
|
193
188
|
end
|
194
189
|
end
|
195
190
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
# REVISIT: This method should verify that all identifying roles (including
|
239
|
-
# those required to identify any superclass) are present (if mandatory)
|
240
|
-
# and are unique... BEFORE it creates any new object(s)
|
241
|
-
# This is a hard problem because it's recursive.
|
242
|
-
def assert_instance(constellation, args) #:nodoc:
|
243
|
-
# Build the key for this instance from the args
|
244
|
-
# The key of an instance is the value or array of keys of the identifying values.
|
245
|
-
# The key values aren't necessarily present in the constellation, even after this.
|
246
|
-
key = identifying_role_values(*args)
|
247
|
-
|
248
|
-
# Find and return an existing instance matching this key
|
249
|
-
instances = constellation.instances[self] # All instances of this class in this constellation
|
250
|
-
instance = instances[key]
|
251
|
-
@created_instances ||= []
|
252
|
-
if instance
|
253
|
-
# raise "Additional role values are ignored when asserting an existing instance" if args[-1].is_a? Hash and !args[-1].empty?
|
254
|
-
assign_additional_roles(instance, args[-1]) if args[-1].is_a? Hash and !args[-1].empty?
|
255
|
-
return instance, key # A matching instance of this class
|
256
|
-
end
|
257
|
-
|
258
|
-
# Now construct each of this object's identifying roles
|
191
|
+
def check_supertype_identifiers_match instance, arg_hash
|
192
|
+
supertypes_transitive.each do |supertype|
|
193
|
+
supertype.identifying_role_names.each do |role_name|
|
194
|
+
next unless arg_hash.include?(role_name) # No contradiction here
|
195
|
+
new_value = arg_hash[role_name]
|
196
|
+
existing_value = instance.send(role_name.to_sym)
|
197
|
+
|
198
|
+
# Quick check for an exact match:
|
199
|
+
next if existing_value == new_value or existing_value.identifying_role_values == new_value
|
200
|
+
|
201
|
+
# Coerce the new value to identifying values for the counterpart role's type:
|
202
|
+
role = supertype.roles(role_name)
|
203
|
+
new_key = role.counterpart.object_type.identifying_role_values(instance.constellation, [new_value])
|
204
|
+
# REVISIT: Check that the next line actually gets hit, otherwise strip it out
|
205
|
+
next if existing_value == new_key # This can happen when the counterpart is a value type
|
206
|
+
|
207
|
+
existing_key = existing_value.identifying_role_values
|
208
|
+
next if existing_key.identifying_role_values == new_key
|
209
|
+
raise TypeConflictException.new(basename, supertype, new_key, existing_key)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# all its candidate keys must match those from the arg_hash.
|
215
|
+
def check_no_supertype_instance_exists constellation, arg_hash
|
216
|
+
supertypes_transitive.each do |supertype|
|
217
|
+
key = supertype.identifying_role_values(constellation, [arg_hash])
|
218
|
+
if constellation.instances[supertype][key]
|
219
|
+
raise TypeMigrationException.new(basename, supertype, key)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# This method receives an array (possibly including a trailing arguments hash)
|
225
|
+
# from which the values of identifying roles must be coerced. Note that when a
|
226
|
+
# value which is not the corrent class is received, we recurse to ask that class
|
227
|
+
# to coerce what we *do* have.
|
228
|
+
# The return value is an array of (and arrays of) raw values, not object instances.
|
229
|
+
#
|
230
|
+
# No new instances may be asserted, nor may any roles of objects in the constellation be changed
|
231
|
+
def identifying_role_values(constellation, args)
|
259
232
|
irns = identifying_role_names
|
260
233
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
#
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
=
|
289
|
-
|
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
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
234
|
+
# Normalise positional arguments into an arguments hash (this changes the passed parameter)
|
235
|
+
arg_hash = args[-1].is_a?(Hash) ? args.pop : {}
|
236
|
+
|
237
|
+
# If the first parameter is an object of type self, its
|
238
|
+
# identifying roles provide any values missing from the array/hash.
|
239
|
+
if args[0].is_a?(self)
|
240
|
+
proto = args.shift
|
241
|
+
end
|
242
|
+
|
243
|
+
# Following arguments provide identifying values in sequence; put them into the hash:
|
244
|
+
irns.each do |role_name|
|
245
|
+
break if args.size == 0
|
246
|
+
arg_hash[role_name] = args.shift
|
247
|
+
end
|
248
|
+
|
249
|
+
# Complain if we have left-over arguments
|
250
|
+
if args.size > 0
|
251
|
+
raise "#{basename} expects only (#{irns*', '}) for its identifier, but you provided additional values #{args.inspect}"
|
252
|
+
end
|
253
|
+
|
254
|
+
# The arg_hash will be used to construct a new instance, if necessary
|
255
|
+
args.push(arg_hash)
|
256
|
+
|
257
|
+
irns.map do |role_name|
|
258
|
+
roles(role_name)
|
259
|
+
end.map do |role|
|
260
|
+
if arg_hash.include?(n = role.name) # Do it this way to avoid problems where nil or false is provided
|
261
|
+
value = arg_hash[n]
|
262
|
+
next (value && true) if (role.is_unary)
|
263
|
+
if value
|
264
|
+
klass = role.counterpart.object_type
|
265
|
+
value = klass.identifying_role_values(constellation, Array(value))
|
266
|
+
end
|
267
|
+
elsif proto
|
268
|
+
value = proto.send(n)
|
269
|
+
arg_hash[n] = value.identifying_role_values # Save the value for making a new instance
|
270
|
+
next value if (role.is_unary)
|
271
|
+
else
|
272
|
+
value = nil
|
273
|
+
end
|
274
|
+
|
275
|
+
raise MissingMandatoryRoleValueException.new(self, role) if value.nil? && role.mandatory
|
276
|
+
|
277
|
+
value
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def assert_instance(constellation, args)
|
282
|
+
key = identifying_role_values(constellation, args)
|
283
|
+
|
284
|
+
# The args is now normalized to an array containing a single Hash element
|
285
|
+
arg_hash = args[-1]
|
286
|
+
|
287
|
+
# Find or make an instance of the class:
|
288
|
+
instance_index = constellation.instances[self] # All instances of this class in this constellation
|
289
|
+
instance = constellation.has_candidate(self, key) || instance_index[key]
|
290
|
+
if (instance)
|
291
|
+
# Check that all assertions about supertype keys are non-contradictory
|
292
|
+
check_supertype_identifiers_match(instance, arg_hash)
|
293
|
+
else
|
294
|
+
# Check that no instance of any supertype matches the keys given
|
295
|
+
check_no_supertype_instance_exists(constellation, arg_hash)
|
296
|
+
|
297
|
+
instance = new_instance(constellation, arg_hash)
|
298
|
+
constellation.candidate(instance)
|
299
|
+
end
|
300
|
+
|
301
|
+
# Assign any extra roles that may have been passed.
|
302
|
+
# An exception here leaves the object indexed,
|
303
|
+
# but without the offending role (re-)assigned.
|
304
|
+
arg_hash.each do |k, v|
|
305
|
+
role = instance.class.roles(k)
|
306
|
+
unless role.is_identifying && role.object_type == self
|
307
|
+
value =
|
308
|
+
if v == nil
|
309
|
+
nil
|
310
|
+
elsif role.is_unary
|
311
|
+
(v && true) # Preserve nil and false
|
312
|
+
else
|
313
|
+
role.counterpart.object_type.assert_instance(constellation, Array(v))
|
314
|
+
end
|
315
|
+
instance.send(:"#{k}=", value)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
instance
|
320
|
+
end
|
321
|
+
|
322
|
+
def index_instance(constellation, instance) #:nodoc:
|
323
|
+
# Index the instance in the constellation's InstanceIndex for this class:
|
324
|
+
instance_index = constellation.instances[self]
|
325
|
+
key = instance.identifying_role_values(self)
|
326
|
+
instance_index[key] = instance
|
342
327
|
|
343
328
|
# Index the instance for each supertype:
|
344
|
-
|
345
|
-
|
346
|
-
|
329
|
+
supertypes.each do |supertype|
|
330
|
+
supertype.index_instance(constellation, instance)
|
331
|
+
end
|
347
332
|
|
348
|
-
|
349
|
-
|
333
|
+
instance
|
334
|
+
end
|
350
335
|
|
351
336
|
# A object_type that isn't a ValueType must have an identification scheme,
|
352
337
|
# which is a list of roles it plays. The identification scheme may be
|
@@ -382,9 +367,16 @@ module ActiveFacts
|
|
382
367
|
end
|
383
368
|
end
|
384
369
|
|
385
|
-
def
|
370
|
+
def self.included other #:nodoc:
|
386
371
|
other.send :extend, ClassMethods
|
387
372
|
|
373
|
+
def other.new_instance constellation, *args
|
374
|
+
instance = allocate
|
375
|
+
instance.instance_variable_set("@constellation", constellation)
|
376
|
+
instance.send(:initialize, *args)
|
377
|
+
instance
|
378
|
+
end
|
379
|
+
|
388
380
|
# Register ourselves with the parent module, which has become a Vocabulary:
|
389
381
|
vocabulary = other.modspace
|
390
382
|
unless vocabulary.respond_to? :object_type # Extend module with Vocabulary if necessary
|