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.
@@ -34,11 +34,26 @@ module ActiveFacts
34
34
  end
35
35
 
36
36
  class DuplicateIdentifyingValueException < ActiveFactsRuntimeException
37
- def initialize(desc)
38
- super("Illegal attempt to assert #{desc[:class].basename} having identifying value" +
39
- " (#{desc[:role].name} is #{desc[:value].verbalise})," +
40
- " when #{desc[:value].related_entities.map(&:verbalise).join(", ")} already exists")
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
@@ -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
- attr_accessor :constellation
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
- # Detect inconsistencies within constellation if this entity was updated
28
- # with the specified role/value pair.
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
- # Checks if instance have duplicate values within its constellation.
42
- #
43
- # Only works on identifying roles.
44
- def duplicate_identifying_values?(role, value)
45
- @constellation && role.is_identifying && !is_unique?(:role => role, :value => value)
46
- end
47
-
48
- # Checks if instance would still be unique if it was updated with
49
- # args.
50
- #
51
- # args should be a hash containing the role and value to update
52
- # and the name of the identifying value as the key.
53
- #
54
- # For example, if a Person is identified by name and family_name:
55
- # updated_values = { :name => "John" }
56
- # Would merge this hash with the one defining the current instance
57
- # and verify in our constellation if it exists.
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
- !duplicate
54
+ raise DuplicateIdentifyingValueException.new(self.class, role.name, value) if duplicate
76
55
  end
77
56
 
78
- # List entities which reference the current one.
79
- #
80
- # Once an entity is found, it will also search for
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
- instance_index_counterpart(role).each do |irv, instance|
85
- if instance.class.is_entity_type && instance.is_identified_by?(self)
86
- if !instances.include?(instance)
87
- instances << instance
88
- instance.related_entities(instances)
89
- end
90
- end
91
- end
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 it remembers our identifying role values
126
- @constellation.__retract(self) if @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
- ([self.class]+self.class.supertypes_transitive).each do |klass|
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
- send role.setter, nil
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
- send(role.name).to_a.each do |v|
142
- if counterpart.is_identifying
143
- v.retract
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
- v.send(counterpart.setter, nil)
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 Instance.included other #:nodoc:
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, :delete_if
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 and Date delegates and hacks to handle immediate types.
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
- raise "AutoCounter #{self.class} may not be #{i.inspect}" unless i == :new or i.is_a?(Integer) or i.is_a?(AutoCounter)
137
- @@placeholder ||= 0
138
- if i == :new
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
- @initially = (@@placeholder+=1)
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
- @initially = @value = i.to_i;
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_#{@initially}"
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
- raise ArgumentError, "Illegal attempt to get integer value of an uncommitted AutoCounter" unless @value
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
- raise ArgumentError, "Illegal attempt to use the value of an uncommitted AutoCounter" unless @value
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
- 0
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
- def other.identifying_role_values(*args)
200
- return nil if args == [:new] # A new object has no identifying_role_values
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