activefacts-api 0.8.12 → 0.9.1

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.
Files changed (36) hide show
  1. data/.rspec +1 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +14 -0
  4. data/Rakefile +21 -9
  5. data/VERSION +1 -1
  6. data/activefacts-api.gemspec +31 -12
  7. data/lib/activefacts/api.rb +1 -0
  8. data/lib/activefacts/api/constellation.rb +3 -1
  9. data/lib/activefacts/api/entity.rb +74 -29
  10. data/lib/activefacts/api/exceptions.rb +17 -0
  11. data/lib/activefacts/api/instance.rb +96 -1
  12. data/lib/activefacts/api/instance_index.rb +35 -37
  13. data/lib/activefacts/api/numeric.rb +62 -56
  14. data/lib/activefacts/api/object_type.rb +49 -23
  15. data/lib/activefacts/api/role.rb +8 -2
  16. data/lib/activefacts/api/role_values.rb +8 -26
  17. data/lib/activefacts/api/standard_types.rb +2 -17
  18. data/lib/activefacts/api/vocabulary.rb +1 -1
  19. data/lib/activefacts/tracer.rb +13 -1
  20. data/spec/{constellation_spec.rb → constellation/constellation_spec.rb} +127 -56
  21. data/spec/constellation/instance_index_spec.rb +90 -0
  22. data/spec/{instance_spec.rb → constellation/instance_spec.rb} +48 -42
  23. data/spec/{role_values_spec.rb → fact_type/role_values_spec.rb} +28 -19
  24. data/spec/{roles_spec.rb → fact_type/roles_spec.rb} +55 -21
  25. data/spec/fixtures/tax.rb +45 -0
  26. data/spec/{identification_spec.rb → identification_scheme/identification_spec.rb} +88 -74
  27. data/spec/identification_scheme/identity_change_spec.rb +118 -0
  28. data/spec/identification_scheme/identity_supertype_change_spec.rb +63 -0
  29. data/spec/{entity_type_spec.rb → object_type/entity_type/entity_type_spec.rb} +2 -4
  30. data/spec/object_type/entity_type/multipart_identification_spec.rb +77 -0
  31. data/spec/{autocounter_spec.rb → object_type/value_type/autocounter_spec.rb} +2 -4
  32. data/spec/object_type/value_type/numeric_spec.rb +63 -0
  33. data/spec/{value_type_spec.rb → object_type/value_type/value_type_spec.rb} +10 -14
  34. data/spec/simplecov_helper.rb +8 -0
  35. data/spec/spec_helper.rb +1 -1
  36. metadata +100 -19
@@ -4,6 +4,9 @@
4
4
  #
5
5
  # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
6
  #
7
+
8
+ require 'forwardable'
9
+
7
10
  module ActiveFacts
8
11
  module API
9
12
  #
@@ -12,6 +15,10 @@ module ActiveFacts
12
15
  # arguments (where ObjectType is the object_type name you're interested in)
13
16
  #
14
17
  class InstanceIndex
18
+ extend Forwardable
19
+ def_delegators :@hash, :size, :empty?, :each, :map,
20
+ :detect, :values, :keys, :detect, :delete_if
21
+
15
22
  def initialize(constellation, klass)
16
23
  @constellation = constellation
17
24
  @klass = klass
@@ -22,44 +29,25 @@ module ActiveFacts
22
29
  "<InstanceIndex for #{@klass.name} in #{@constellation.inspect}>"
23
30
  end
24
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.
25
37
  def assert(*args)
26
- #trace :assert, "Asserting #{@klass} with #{args.inspect}" do
27
- instance, key = *@klass.assert_instance(@constellation, args)
28
- instance
29
- #end
38
+ instance, key = *@klass.assert_instance(@constellation, args)
39
+ @klass.created_instances = nil if instance.class.is_entity_type
40
+ instance
30
41
  end
31
42
 
32
43
  def include?(*args)
33
44
  if args.size == 1 && args[0].is_a?(@klass)
34
45
  key = args[0].identifying_role_values
35
46
  else
36
- key = @klass.identifying_role_values(*args)
47
+ key = @klass.identifying_role_values(*args) rescue nil
37
48
  end
38
- return @hash[key]
39
- end
40
-
41
- def []=(key, value) #:nodoc:
42
- @hash[key] = value
43
- end
44
-
45
- def [](*args)
46
- @hash[*args]
47
- end
48
-
49
- def size
50
- @hash.size
51
- end
52
-
53
- def empty?
54
- @hash.size == 0
55
- end
56
-
57
- def each &b
58
- @hash.each &b
59
- end
60
49
 
61
- def map &b
62
- @hash.map &b
50
+ @hash[key]
63
51
  end
64
52
 
65
53
  def detect &b
@@ -67,18 +55,28 @@ module ActiveFacts
67
55
  r ? r[1] : nil
68
56
  end
69
57
 
70
- # Return an array of all the instances of this object_type
71
- def values
72
- @hash.values
58
+ def []=(key, value) #:nodoc:
59
+ @hash[flatten_key(key)] = value
60
+ end
61
+
62
+ def [](key)
63
+ @hash[flatten_key(key)]
73
64
  end
74
65
 
75
- # Return an array of the identifying role values arrays for all the instances of this object_type
76
- def keys
77
- @hash.keys
66
+ def refresh_key(key)
67
+ value = @hash.delete(key)
68
+ @hash[value.identifying_role_values] = value if value
78
69
  end
79
70
 
80
- def delete_if(&b) #:nodoc:
81
- @hash.delete_if &b
71
+ private
72
+ def flatten_key(key)
73
+ if key.is_a?(Array)
74
+ key.map { |identifier| flatten_key(identifier) }
75
+ elsif key.respond_to?(:identifying_role_values)
76
+ key.identifying_role_values
77
+ else
78
+ key
79
+ end
82
80
  end
83
81
  end
84
82
  end
@@ -10,79 +10,82 @@
10
10
  #
11
11
  require 'delegate'
12
12
  require 'date'
13
+ require 'bigdecimal'
14
+
15
+ module ActiveFacts
16
+ module API
17
+ # Fixes behavior of core functions over multiple platform
18
+ module SimpleDelegation
19
+ def initialize(v)
20
+ __setobj__(delegate_new(v))
21
+ end
13
22
 
14
- # It's not possible to subclass Integer, so instead we delegate to it.
15
- class Int < SimpleDelegator
16
- def initialize(i = nil) #:nodoc:
17
- __setobj__(Integer(i))
18
- end
23
+ def eql?(v)
24
+ # Note: This and #hash do not work the way you'd expect,
25
+ # and differently in each Ruby interpreter. If you store
26
+ # an Int or Real in a hash, you cannot reliably retrieve
27
+ # them with the corresponding Integer or Real.
28
+ __getobj__.eql?(delegate_new(v))
29
+ end
19
30
 
20
- def to_s #:nodoc:
21
- __getobj__.to_s
22
- end
31
+ def ==(o) #:nodoc:
32
+ __getobj__.==(o)
33
+ end
23
34
 
24
- def to_json c = {} #:nodoc
25
- __getobj__.to_json
26
- end
35
+ def to_s *a #:nodoc:
36
+ __getobj__.to_s *a
37
+ end
27
38
 
28
- def hash #:nodoc:
29
- __getobj__.hash
30
- end
39
+ def to_json(*a) #:nodoc:
40
+ __getobj__.to_s
41
+ end
31
42
 
32
- def eql?(o) #:nodoc:
33
- # Note: This and #hash do not work the way you'd expect,
34
- # and differently in each Ruby interpreter. If you store
35
- # an Int or Real in a hash, you cannot reliably retrieve
36
- # them with the corresponding Integer or Real.
37
- __getobj__.eql?(Integer(o))
38
- end
43
+ def hash #:nodoc:
44
+ __getobj__.hash
45
+ end
39
46
 
40
- def ==(o) #:nodoc:
41
- __getobj__.==(o)
42
- end
47
+ def is_a?(k)
48
+ __getobj__.is_a?(k) || super
49
+ end
43
50
 
44
- def is_a?(k)
45
- __getobj__.is_a?(k) || super
46
- end
51
+ def kind_of?(k)
52
+ is_a?(k)
53
+ end
47
54
 
48
- def inspect
49
- "#{self.class.basename}:#{__getobj__.inspect}"
55
+ def inspect
56
+ "#{self.class.basename}:#{__getobj__.inspect}"
57
+ end
58
+ end
50
59
  end
51
60
  end
52
61
 
53
- # It's not possible to subclass Float, so instead we delegate to it.
54
- class Real < SimpleDelegator
55
- def initialize(r = nil) #:nodoc:
56
- __setobj__(Float(r))
57
- end
62
+ class Decimal < SimpleDelegator #:nodoc:
63
+ include ActiveFacts::API::SimpleDelegation
58
64
 
59
- def hash #:nodoc:
60
- __getobj__.hash
61
- end
62
-
63
- def to_s #:nodoc:
64
- __getobj__.to_s
65
- end
66
-
67
- def to_json c = {} #:nodoc
68
- __getobj__.to_json
65
+ def delegate_new(v)
66
+ if v.is_a?(BigDecimal) || v.is_a?(Bignum)
67
+ BigDecimal.new(v.to_s)
68
+ else
69
+ BigDecimal.new(v)
70
+ end
69
71
  end
72
+ end
70
73
 
71
- def eql?(o) #:nodoc:
72
- # Note: See the note above on Int#eql?
73
- __getobj__.eql?(Float(o))
74
- end
74
+ # It's not possible to subclass Integer, so instead we delegate to it.
75
+ class Int < SimpleDelegator
76
+ include ActiveFacts::API::SimpleDelegation
75
77
 
76
- def ==(o) #:nodoc:
77
- __getobj__.==(o)
78
+ def delegate_new(i = nil) #:nodoc:
79
+ Integer(i)
78
80
  end
81
+ end
79
82
 
80
- def is_a?(k)
81
- __getobj__.is_a?(k) || super
82
- end
83
+ # It's not possible to subclass Float, so instead we delegate to it.
84
+ class Real < SimpleDelegator
85
+ include ActiveFacts::API::SimpleDelegation
83
86
 
84
- def inspect #:nodoc:
85
- "#{self.class.basename}:#{__getobj__.inspect}"
87
+ def delegate_new(r = nil) #:nodoc:
88
+ Float(r)
86
89
  end
87
90
  end
88
91
 
@@ -189,7 +192,10 @@ class AutoCounter
189
192
  def self.inherited(other) #:nodoc:
190
193
  def other.identifying_role_values(*args)
191
194
  return nil if args == [:new] # A new object has no identifying_role_values
192
- return args[0] if args.size == 1 and args[0].is_a?(AutoCounter)
195
+ if args.size == 1
196
+ return args[0] if args[0].is_a?(AutoCounter)
197
+ return args[0].send(self.basename.snakecase.to_sym) if args[0].respond_to?(self.basename.snakecase.to_sym)
198
+ end
193
199
  return new(*args)
194
200
  end
195
201
  super
@@ -46,7 +46,7 @@ module ActiveFacts
46
46
  #
47
47
  # Example: maybe :is_ceo
48
48
  def maybe(role_name)
49
- realise_role(roles[role_name] = Role.new(self, nil, role_name))
49
+ realise_role(roles[role_name] = Role.new(self, TrueClass, role_name))
50
50
  end
51
51
 
52
52
  # Define a binary fact type relating this object_type to another,
@@ -61,6 +61,7 @@ module ActiveFacts
61
61
  # * :restrict - a list of values or ranges which this role may take. Not used yet.
62
62
  def has_one(role_name, options = {})
63
63
  role_name, related, mandatory, related_role_name = extract_binary_params(false, role_name, options)
64
+ detect_fact_type_collision(:type => :has_one, :role => role_name, :related => related)
64
65
  define_binary_fact_type(false, role_name, related, mandatory, related_role_name)
65
66
  end
66
67
 
@@ -77,9 +78,25 @@ module ActiveFacts
77
78
  def one_to_one(role_name, options = {})
78
79
  role_name, related, mandatory, related_role_name =
79
80
  extract_binary_params(true, role_name, options)
81
+ detect_fact_type_collision(:type => :one_to_one, :role => role_name, :related => related)
80
82
  define_binary_fact_type(true, role_name, related, mandatory, related_role_name)
81
83
  end
82
84
 
85
+ def detect_fact_type_collision(fact)
86
+ if respond_to?(:identifying_role_names) && identifying_role_names.include?(fact[:role])
87
+ case fact[:type]
88
+ when :has_one
89
+ if identifying_role_names.size == 1
90
+ raise "Entity type #{self} cannot be identified by a single role '#{fact[:role]}' unless that role is one_to_one"
91
+ end
92
+ when :one_to_one
93
+ if identifying_role_names.size > 1
94
+ raise "Entity type #{self} cannot be identified by a single role '#{fact[:role]}' unless that role is has_one"
95
+ end
96
+ end
97
+ end
98
+ end
99
+
83
100
  # Access supertypes or add new supertypes; multiple inheritance.
84
101
  # With parameters (Class objects), it adds new supertypes to this class.
85
102
  # Instances of this class will then have role methods for any new superclasses (transitively).
@@ -140,7 +157,7 @@ module ActiveFacts
140
157
 
141
158
  # Every new role added or inherited comes through here:
142
159
  def realise_role(role) #:nodoc:
143
- if (!role.counterpart)
160
+ if (role.is_unary)
144
161
  # Unary role
145
162
  define_unary_role_accessor(role)
146
163
  elsif (role.unique)
@@ -229,22 +246,22 @@ module ActiveFacts
229
246
 
230
247
  class_eval do
231
248
  define_method role.setter do |value|
232
- role_var = role.variable
233
249
 
234
- # Get old value, and jump out early if it's unchanged:
235
- old = instance_variable_get(role_var) rescue nil
236
- return value if old.equal?(value) # Occurs when another instance having the same value is assigned
250
+ old = instance_variable_get(role.variable) rescue nil
251
+ return true if old.equal?(value) # Occurs when another instance having the same value is assigned
237
252
 
238
- value = role.adapt(constellation, value) if value
239
- return value if old.equal?(value) # Occurs when same value but not same instance is assigned
253
+ value = role.adapt(@constellation, value) if value
254
+ return true if old.equal?(value) # Occurs when same value but not same instance is assigned
240
255
 
241
- # REVISIT: A frozen-key solution could be used to allow changing identifying roles.
242
- # If this object plays an identifying role in other objects, they also need re-indexing
243
- # if role.is_identifying
244
- # raise "#{self.class.basename}: illegal attempt to modify identifying role #{role.name}" if value != nil && old != nil
245
- # end
256
+ detect_inconsistencies(role, value)
246
257
 
247
- instance_variable_set(role_var, value)
258
+ if @constellation && old
259
+ keys = old.related_entities.map do |entity|
260
+ [entity.identifying_role_values, entity]
261
+ end
262
+ end
263
+
264
+ instance_variable_set(role.variable, value)
248
265
 
249
266
  # Remove self from the old counterpart:
250
267
  old.send(role.counterpart.setter, nil) if old
@@ -252,6 +269,12 @@ module ActiveFacts
252
269
  # Assign self to the new counterpart
253
270
  value.send(role.counterpart.setter, self) if value
254
271
 
272
+ if keys
273
+ keys.each do |key, entity|
274
+ entity.instance_index.refresh_key(key)
275
+ end
276
+ end
277
+
255
278
  value
256
279
  end
257
280
  end
@@ -271,16 +294,13 @@ module ActiveFacts
271
294
  value = role.adapt(constellation, value) if value
272
295
  return value if old.equal?(value) # Occurs when another instance having the same value is assigned
273
296
 
274
- # REVISIT: A frozen-key solution could be used to allow changing identifying roles.
275
- # If this object plays an identifying role in other objects, they need re-indexing
276
- # The key would be frozen, allowing indices and counterparts to de-assign,
277
- # but delay re-assignment until defrosted.
278
- # That would also allow caching the identifying_role_values, a performance win.
297
+ detect_inconsistencies(role, value) if value
279
298
 
280
- # This allows setting and clearing identifying roles, but not changing them.
281
- # if role.is_identifying
282
- # raise "#{self.class.basename}: illegal attempt to modify identifying role #{role.name}" if value != nil && old != nil
283
- # end
299
+ if old && old.constellation
300
+ keys = old.related_entities.map do |entity|
301
+ [entity.identifying_role_values, entity]
302
+ end
303
+ end
284
304
 
285
305
  instance_variable_set(role_var, value)
286
306
 
@@ -290,6 +310,12 @@ module ActiveFacts
290
310
  # Add "self" into the counterpart
291
311
  value.send(getter ||= role.counterpart.getter).update(old, self) if value
292
312
 
313
+ if keys
314
+ keys.each do |key, entity|
315
+ entity.instance_index.refresh_key(key)
316
+ end
317
+ end
318
+
293
319
  value
294
320
  end
295
321
  end
@@ -15,6 +15,7 @@ module ActiveFacts
15
15
  # Each ObjectType class maintains a RoleCollection hash of the roles it plays.
16
16
  class Role
17
17
  attr_reader :object_type # The ObjectType to which this role belongs
18
+ attr_reader :is_unary
18
19
  attr_reader :name # The name of the role (a Symbol)
19
20
  attr_accessor :counterpart # All roles except unaries have a counterpart Role
20
21
  attr_reader :unique # Is this role played by at most one instance, or more?
@@ -24,7 +25,8 @@ module ActiveFacts
24
25
 
25
26
  def initialize(object_type, counterpart, name, mandatory = false, unique = true)
26
27
  @object_type = object_type
27
- @counterpart = counterpart
28
+ @is_unary = counterpart == TrueClass
29
+ @counterpart = @is_unary ? nil : counterpart
28
30
  @name = name
29
31
  @mandatory = mandatory
30
32
  @unique = unique
@@ -53,9 +55,13 @@ module ActiveFacts
53
55
  counterpart == nil
54
56
  end
55
57
 
58
+ def is_inherited?(klass)
59
+ klass.supertypes_transitive.include?(@object_type)
60
+ end
61
+
56
62
  def counterpart_object_type
57
63
  # This method is sometimes used when unaries are used in an entity's identifier.
58
- counterpart == nil ? TrueClass : counterpart.object_type
64
+ @is_unary ? TrueClass : (counterpart ? counterpart.object_type : nil)
59
65
  end
60
66
 
61
67
  def inspect
@@ -4,44 +4,27 @@
4
4
  #
5
5
  # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
6
  #
7
- # There are two implementations here, one using an array and one using a hash.
8
- # The hash one has problems with keys being changed during object deletion, so
9
- # cannot be used yet; a fix is upcoming and will improve performance of large sets.
10
- #
7
+ require 'forwardable'
8
+
11
9
  module ActiveFacts
12
10
  module API
13
11
 
14
12
  class RoleValues #:nodoc:
15
13
  include Enumerable
14
+ extend Forwardable
15
+
16
+ def_delegators :@a, :each, :size, :empty?, :-
16
17
 
17
18
  def initialize
18
19
  @a = []
19
20
  end
20
21
 
21
- def each &b
22
- # REVISIT: Provide a configuration variable to enable this heckling during testing:
23
- #@a.sort_by{rand}.each &b
24
- @a.each &b
25
- end
26
-
27
- def size
28
- @a.size
29
- end
30
-
31
- def empty?
32
- @a.size == 0
33
- end
34
-
35
22
  def +(a)
36
- @a.+(a.is_a?(RoleValues) ? Array(a) : a)
37
- end
38
-
39
- def -(a)
40
- @a - a
23
+ @a.+(a.is_a?(RoleValues) ? [a] : a)
41
24
  end
42
25
 
43
26
  def single
44
- @a.size > 1 ? nil : @a[0]
27
+ size > 1 ? nil : @a[0]
45
28
  end
46
29
 
47
30
  def update(old, value)
@@ -50,9 +33,8 @@ module ActiveFacts
50
33
  end
51
34
 
52
35
  def verbalise
53
- "["+@a.to_a.map{|e| e.verbalise}*", "+"]"
36
+ "[#{@a.map(&:verbalise).join(", ")}]"
54
37
  end
55
-
56
38
  end
57
39
 
58
40
  end