activefacts-api 0.8.12 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
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