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
@@ -34,11 +34,26 @@ module ActiveFacts
|
|
34
34
|
end
|
35
35
|
|
36
36
|
class DuplicateIdentifyingValueException < ActiveFactsRuntimeException
|
37
|
-
def initialize(
|
38
|
-
super("Illegal attempt to assert #{
|
39
|
-
" (#{
|
40
|
-
" when #{
|
37
|
+
def initialize(klass, role_name, value)
|
38
|
+
super("Illegal attempt to assert #{klass.basename} having identifying value" +
|
39
|
+
" (#{role_name} is #{value.verbalise})," +
|
40
|
+
" when #{value.related_entities(false).map(&:verbalise).join(", ")} already exists")
|
41
41
|
end
|
42
42
|
end
|
43
|
+
|
44
|
+
# When an existing object having multiple identification patterns is re-asserted, all the keys must match the existing object
|
45
|
+
class TypeConflictException < ActiveFactsRuntimeException
|
46
|
+
def initialize(klass, supertype, key, existing)
|
47
|
+
super "#{klass} cannot be asserted to have #{supertype} identifier #{key.inspect} because the existing object has #{existing.inspect}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# When a new entity is asserted, but a supertype identifier matches an existing object of a different type, type migration is implied but unfortunately is impossible in Ruby
|
52
|
+
class TypeMigrationException < ActiveFactsRuntimeException
|
53
|
+
def initialize(klass, supertype, key)
|
54
|
+
super "#{klass} cannot be asserted due to the prior existence of a conflicting #{supertype} identified by #{key.inspect}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
43
58
|
end
|
44
59
|
end
|
data/lib/activefacts/api/guid.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'delegate'
|
2
|
-
require 'date'
|
3
2
|
require 'securerandom'
|
4
3
|
|
5
4
|
unless defined? SecureRandom.uuid
|
@@ -30,6 +29,10 @@ class Guid
|
|
30
29
|
@value == value
|
31
30
|
end
|
32
31
|
|
32
|
+
def == value
|
33
|
+
@value == value.to_s
|
34
|
+
end
|
35
|
+
|
33
36
|
def inspect
|
34
37
|
"\#<Guid #{@value}>"
|
35
38
|
end
|
@@ -42,16 +45,4 @@ class Guid
|
|
42
45
|
to_s.eql?(o.to_s)
|
43
46
|
end
|
44
47
|
|
45
|
-
# def self.inherited(other) #:nodoc:
|
46
|
-
# def other.identifying_role_values(*args)
|
47
|
-
# return nil if args == [:new] # A new object has no identifying_role_values
|
48
|
-
# if args.size == 1
|
49
|
-
# return args[0] if args[0].is_a?(AutoCounter)
|
50
|
-
# return args[0].send(self.basename.snakecase.to_sym) if args[0].respond_to?(self.basename.snakecase.to_sym)
|
51
|
-
# end
|
52
|
-
# return new(*args)
|
53
|
-
# end
|
54
|
-
# super
|
55
|
-
# end
|
56
|
-
|
57
48
|
end
|
@@ -11,12 +11,16 @@ module ActiveFacts
|
|
11
11
|
# Every Instance of a ObjectType (A Value type or an Entity type) includes the methods of this module:
|
12
12
|
module Instance
|
13
13
|
# What constellation does this Instance belong to (if any):
|
14
|
-
|
14
|
+
attr_reader :constellation
|
15
15
|
|
16
16
|
def initialize(args = []) #:nodoc:
|
17
17
|
unless (self.class.is_entity_type)
|
18
18
|
begin
|
19
19
|
super(*args)
|
20
|
+
rescue TypeError => e
|
21
|
+
if trace(:debug)
|
22
|
+
p e; puts e.backtrace*"\n\t"; debugger; true
|
23
|
+
end
|
20
24
|
rescue ArgumentError => e
|
21
25
|
e.message << " constructing a #{self.class}"
|
22
26
|
raise
|
@@ -24,125 +28,83 @@ module ActiveFacts
|
|
24
28
|
end
|
25
29
|
end
|
26
30
|
|
27
|
-
|
28
|
-
|
29
|
-
def detect_inconsistencies(role, value)
|
30
|
-
if duplicate_identifying_values?(role, value)
|
31
|
-
exception_data = {
|
32
|
-
:value => value,
|
33
|
-
:role => role,
|
34
|
-
:class => self.class
|
35
|
-
}
|
36
|
-
|
37
|
-
raise DuplicateIdentifyingValueException.new(exception_data)
|
38
|
-
end
|
31
|
+
def is_a? klass
|
32
|
+
super || self.class.supertypes_transitive.include?(klass)
|
39
33
|
end
|
40
34
|
|
41
|
-
#
|
42
|
-
#
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
#
|
59
|
-
# The uniqueness of the entity will also be checked within its supertypes.
|
60
|
-
#
|
61
|
-
# An Employee -subtype of a Person- identified by its employee_id would
|
62
|
-
# collide with a Person if it has the same name. But `name` may not be
|
63
|
-
# an identifying value for the Employee identification scheme.
|
64
|
-
def is_unique?(args)
|
65
|
-
duplicate = ([self.class] + self.class.supertypes_transitive).detect do |klass|
|
66
|
-
old_identity = identity_by(klass)
|
67
|
-
if klass.identifying_roles.include?(args[:role])
|
68
|
-
new_identity = old_identity.merge(args[:role].getter => args[:value])
|
69
|
-
@constellation.instances[klass].include?(new_identity)
|
70
|
-
else
|
71
|
-
false
|
72
|
-
end
|
35
|
+
# If this instance's role is updated to the new value, does that cause a collision?
|
36
|
+
# We need to check each superclass that has a different identification pattern
|
37
|
+
def check_value_change_legality(role, value)
|
38
|
+
return unless @constellation && role.is_identifying
|
39
|
+
|
40
|
+
klasses = [self.class] + self.class.supertypes_transitive
|
41
|
+
last_identity = nil
|
42
|
+
last_irns = nil
|
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
|
50
|
+
end
|
51
|
+
@constellation.instances[klass][last_identity]
|
73
52
|
end
|
74
53
|
|
75
|
-
|
54
|
+
raise DuplicateIdentifyingValueException.new(self.class, role.name, value) if duplicate
|
76
55
|
end
|
77
56
|
|
78
|
-
# List entities which
|
79
|
-
|
80
|
-
|
81
|
-
# related entities of this instance.
|
82
|
-
def related_entities(instances = [])
|
57
|
+
# List entities which have an identifying role played by this object.
|
58
|
+
def related_entities(indirectly = true, instances = [])
|
59
|
+
# Check all roles of this instance
|
83
60
|
self.class.roles.each do |role_name, role|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
61
|
+
# If the counterpart role is not identifying for its object type, skip it
|
62
|
+
next unless c = role.counterpart and c.is_identifying
|
63
|
+
|
64
|
+
identified_instances = Array(self.send(role.getter))
|
65
|
+
instances.concat(identified_instances)
|
66
|
+
identified_instances.each do |instance|
|
67
|
+
instance.related_entities(indirectly, instances) if indirectly
|
68
|
+
end
|
92
69
|
end
|
93
70
|
instances
|
94
71
|
end
|
95
72
|
|
96
|
-
# Determine if entity is an identifying value
|
97
|
-
# of the current instance.
|
98
|
-
def is_identified_by?(entity)
|
99
|
-
self.class.identifying_roles.detect do |role|
|
100
|
-
send(role.getter) == entity
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
73
|
def instance_index
|
105
74
|
@constellation.send(self.class.basename.to_sym)
|
106
75
|
end
|
107
76
|
|
108
|
-
def instance_index_counterpart(role)
|
109
|
-
if @constellation && role.counterpart
|
110
|
-
@constellation.send(role.counterpart.object_type.basename.to_sym)
|
111
|
-
else
|
112
|
-
[]
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
# Verbalise this instance
|
117
|
-
# REVISIT: Should it raise an error if it was not redefined ?
|
118
|
-
def verbalise
|
119
|
-
# REVISIT: Should it raise an error if it was not redefined ?
|
120
|
-
# This method should always be overridden in subclasses
|
121
|
-
end
|
122
|
-
|
123
77
|
# De-assign all functional roles and remove from constellation, if any.
|
124
78
|
def retract
|
125
|
-
# Delete from the constellation first, while
|
126
|
-
@constellation.
|
79
|
+
# Delete from the constellation first, while we remember our identifying role values
|
80
|
+
@constellation.deindex_instance(self) if @constellation
|
127
81
|
|
128
82
|
# Now, for all roles (from this class and all supertypes), assign nil to all functional roles
|
129
83
|
# The counterpart roles get cleared automatically.
|
130
|
-
|
84
|
+
klasses = [self.class]+self.class.supertypes_transitive
|
85
|
+
klasses.each do |klass|
|
131
86
|
klass.roles.each do |role_name, role|
|
132
87
|
next if role.unary?
|
133
88
|
counterpart = role.counterpart
|
134
|
-
if role.unique
|
135
|
-
# puts "Nullifying mandatory role #{role.name} of #{role.object_type.name}" if counterpart.mandatory
|
136
89
|
|
137
|
-
|
90
|
+
# Objects being created do not have to have non-identifying mandatory roles,
|
91
|
+
# so we allow retracting to the same state.
|
92
|
+
if role.unique
|
93
|
+
if counterpart.is_identifying && counterpart.mandatory
|
94
|
+
i = send(role.name) and i.retract
|
95
|
+
else
|
96
|
+
send role.setter, nil
|
97
|
+
end
|
138
98
|
else
|
139
99
|
# puts "Not removing role #{role_name} from counterpart RoleValues #{counterpart.name}"
|
140
100
|
# Duplicate the array using to_a, as the RoleValues here will be modified as we traverse it:
|
141
|
-
|
142
|
-
|
143
|
-
|
101
|
+
counterpart_instances = send(role.name)
|
102
|
+
counterpart_instances.to_a.each do |counterpart_instance|
|
103
|
+
# These actions deconstruct the RoleValues as we go:
|
104
|
+
if counterpart.is_identifying && counterpart.mandatory
|
105
|
+
counterpart_instance.retract
|
144
106
|
else
|
145
|
-
|
107
|
+
counterpart_instance.send(counterpart.setter, nil)
|
146
108
|
end
|
147
109
|
end
|
148
110
|
end
|
@@ -155,7 +117,7 @@ module ActiveFacts
|
|
155
117
|
# Add Instance class methods here
|
156
118
|
end
|
157
119
|
|
158
|
-
def
|
120
|
+
def self.included other #:nodoc:
|
159
121
|
other.send :extend, ClassMethods
|
160
122
|
end
|
161
123
|
end
|
@@ -17,7 +17,7 @@ module ActiveFacts
|
|
17
17
|
class InstanceIndex
|
18
18
|
extend Forwardable
|
19
19
|
def_delegators :@hash, :size, :empty?, :each, :map,
|
20
|
-
:detect, :values, :keys, :detect, :
|
20
|
+
:detect, :values, :keys, :detect, :delete
|
21
21
|
|
22
22
|
def initialize(constellation, klass)
|
23
23
|
@constellation = constellation
|
@@ -29,37 +29,6 @@ module ActiveFacts
|
|
29
29
|
"<InstanceIndex for #{@klass.name} in #{@constellation.inspect}>"
|
30
30
|
end
|
31
31
|
|
32
|
-
# Assertion of an entity type or a value type
|
33
|
-
#
|
34
|
-
# When asserting an entity type, multiple entity type or value type
|
35
|
-
# may be created. Every instance (entity or value) created in this
|
36
|
-
# process will be removed if the entity type fail to be asserted.
|
37
|
-
def assert(*args)
|
38
|
-
instance, key = *@klass.assert_instance(@constellation, args)
|
39
|
-
@klass.created_instances = nil if instance.class.is_entity_type
|
40
|
-
instance
|
41
|
-
end
|
42
|
-
|
43
|
-
def include?(*args)
|
44
|
-
if args.size == 1 && args[0].is_a?(@klass)
|
45
|
-
key = args[0].identifying_role_values
|
46
|
-
else
|
47
|
-
begin
|
48
|
-
key = @klass.identifying_role_values(*args)
|
49
|
-
rescue TypeError => e
|
50
|
-
# This happens (and should not) during assert_instance when checking
|
51
|
-
# for new asserts of identifying values that might get rolled back
|
52
|
-
# when the assert fails (for example because of an implied subtyping change)
|
53
|
-
key = nil
|
54
|
-
rescue ActiveFactsRuntimeException => e
|
55
|
-
# This is currently only known to happen during a retract()
|
56
|
-
key = nil
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
@hash[key]
|
61
|
-
end
|
62
|
-
|
63
32
|
def detect &b
|
64
33
|
r = @hash.detect &b
|
65
34
|
r ? r[1] : nil
|
@@ -1,6 +1,6 @@
|
|
1
1
|
#
|
2
2
|
# ActiveFacts Runtime API
|
3
|
-
# Numeric
|
3
|
+
# Numeric delegates and hacks to handle immediate types.
|
4
4
|
#
|
5
5
|
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
6
6
|
#
|
@@ -9,7 +9,6 @@
|
|
9
9
|
# Date and DateTime don't have a sensible new() method, so we monkey-patch one here.
|
10
10
|
#
|
11
11
|
require 'delegate'
|
12
|
-
require 'date'
|
13
12
|
require 'bigdecimal'
|
14
13
|
|
15
14
|
module ActiveFacts
|
@@ -89,57 +88,50 @@ class Real < SimpleDelegator
|
|
89
88
|
end
|
90
89
|
end
|
91
90
|
|
92
|
-
# A Date can be constructed from any Date subclass, not just using the normal date constructors.
|
93
|
-
class ::Date
|
94
|
-
class << self; alias_method :old_new, :new end
|
95
|
-
# Date.new cannot normally be called passing a Date as the parameter. This allows that.
|
96
|
-
def self.new(*a, &b)
|
97
|
-
if (a.size == 1 && a[0].is_a?(Date))
|
98
|
-
a = a[0]
|
99
|
-
civil(a.year, a.month, a.day, a.start)
|
100
|
-
elsif (a.size == 1 && a[0].is_a?(String))
|
101
|
-
parse(a[0])
|
102
|
-
else
|
103
|
-
a = [] if a == [nil]
|
104
|
-
civil(*a, &b)
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
# A DateTime can be constructed from any Date or DateTime subclass
|
110
|
-
class ::DateTime
|
111
|
-
class << self; alias_method :old_new, :new end
|
112
|
-
# DateTime.new cannot normally be called passing a Date or DateTime as the parameter. This allows that.
|
113
|
-
def self.new(*a, &b)
|
114
|
-
if (a.size == 1)
|
115
|
-
a = a[0]
|
116
|
-
if (DateTime === a)
|
117
|
-
civil(a.year, a.month, a.day, a.hour, a.min, a.sec, a.start)
|
118
|
-
elsif (Date === a)
|
119
|
-
civil(a.year, a.month, a.day, 0, 0, 0, a.start)
|
120
|
-
else
|
121
|
-
civil(*a, &b)
|
122
|
-
end
|
123
|
-
else
|
124
|
-
civil(*a, &b)
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
91
|
# The AutoCounter class is an integer, but only after the value
|
130
92
|
# has been established in the database.
|
131
93
|
# Construct it with the value :new to get an uncommitted value.
|
132
94
|
# You can use this new instance as a value of any role of this type, including to identify an entity instance.
|
133
95
|
# The assigned value will be filled out everywhere it needs to be, upon save.
|
96
|
+
module ActiveFacts
|
97
|
+
module AutoCounterClass
|
98
|
+
def identifying_role_values(constellation, args)
|
99
|
+
arg_hash = args[-1].is_a?(Hash) ? args.pop : {}
|
100
|
+
n =
|
101
|
+
case
|
102
|
+
when args == [:new] # A new object has no identifying_role_values
|
103
|
+
:new
|
104
|
+
when args.size == 1 && args[0].is_a?(AutoCounter)
|
105
|
+
args[0] # An AutoCounter is its own key
|
106
|
+
else
|
107
|
+
new(*args)
|
108
|
+
end
|
109
|
+
args.replace([arg_hash])
|
110
|
+
n
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
134
115
|
class AutoCounter
|
116
|
+
attr_reader :place_holder_number
|
135
117
|
def initialize(i = :new)
|
136
|
-
|
137
|
-
|
138
|
-
|
118
|
+
unless i == :new or i.is_a?(Integer) or i.is_a?(AutoCounter)
|
119
|
+
raise "AutoCounter #{self.class} may not be #{i.inspect}"
|
120
|
+
end
|
121
|
+
@@place_holder ||= 0
|
122
|
+
case i
|
123
|
+
when :new
|
139
124
|
@value = nil
|
140
|
-
@
|
125
|
+
@place_holder_number = (@@place_holder+=1)
|
126
|
+
when AutoCounter
|
127
|
+
if i.defined?
|
128
|
+
@value = i.to_i
|
129
|
+
else
|
130
|
+
@place_holder_number = i.place_holder_number
|
131
|
+
@value = nil
|
132
|
+
end
|
141
133
|
else
|
142
|
-
@
|
134
|
+
@place_holder_number = @value = i.to_i;
|
143
135
|
end
|
144
136
|
end
|
145
137
|
|
@@ -158,7 +150,7 @@ class AutoCounter
|
|
158
150
|
if self.defined?
|
159
151
|
@value.to_s
|
160
152
|
else
|
161
|
-
"new_#{@
|
153
|
+
"new_#{@place_holder_number}"
|
162
154
|
end
|
163
155
|
end
|
164
156
|
|
@@ -169,13 +161,17 @@ class AutoCounter
|
|
169
161
|
|
170
162
|
# An AutoCounter may only be used in numeric expressions after a definite value has been assigned
|
171
163
|
def to_i
|
172
|
-
|
164
|
+
unless @value
|
165
|
+
raise ArgumentError, "Illegal attempt to get integer value of an uncommitted AutoCounter"
|
166
|
+
end
|
173
167
|
@value
|
174
168
|
end
|
175
169
|
|
176
170
|
# Coerce "i" to be of the same type as self
|
177
171
|
def coerce(i)
|
178
|
-
|
172
|
+
unless @value
|
173
|
+
raise ArgumentError, "Illegal attempt to use the value of an uncommitted AutoCounter"
|
174
|
+
end
|
179
175
|
[ i.to_i, @value ]
|
180
176
|
end
|
181
177
|
|
@@ -187,7 +183,7 @@ class AutoCounter
|
|
187
183
|
if self.defined?
|
188
184
|
@value.hash
|
189
185
|
else
|
190
|
-
|
186
|
+
@place_holder_number
|
191
187
|
end
|
192
188
|
end
|
193
189
|
|
@@ -195,14 +191,14 @@ class AutoCounter
|
|
195
191
|
to_s.eql?(o.to_s)
|
196
192
|
end
|
197
193
|
|
194
|
+
def identifying_role_values
|
195
|
+
self
|
196
|
+
end
|
197
|
+
|
198
|
+
# extend ActiveFacts::AutoCounterClass
|
198
199
|
def self.inherited(other) #:nodoc:
|
199
|
-
|
200
|
-
|
201
|
-
if args.size == 1
|
202
|
-
return args[0] if args[0].is_a?(AutoCounter)
|
203
|
-
return args[0].send(self.basename.snakecase.to_sym) if args[0].respond_to?(self.basename.snakecase.to_sym)
|
204
|
-
end
|
205
|
-
return new(*args)
|
200
|
+
other.class_eval do
|
201
|
+
extend ActiveFacts::AutoCounterClass
|
206
202
|
end
|
207
203
|
super
|
208
204
|
end
|