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.
@@ -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