activefacts-api 0.8.9 → 0.8.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,26 +11,40 @@ module ActiveFacts
11
11
 
12
12
  # A Role represents the relationship of one object to another (or to a boolean condition).
13
13
  # Relationships (or binary fact types) have a Role at each end; one is declared using _has_one_
14
- # or _one_to_one_, and the other is created on the counterpart class. Each ObjectType class maintains
15
- # an array of the roles it plays.
14
+ # or _one_to_one_, and the other is created on the counterpart class.
15
+ # Each ObjectType class maintains a RoleCollection hash of the roles it plays.
16
16
  class Role
17
- attr_accessor :owner # The ObjectType to which this role belongs
18
- attr_accessor :name # The name of the role (a Symbol)
19
- attr_accessor :counterpart_object_type # A ObjectType Class (may be temporarily a Symbol before the class is defined)
20
- attr_accessor :counterpart # All roles except unaries have a binary counterpart
21
- attr_accessor :unique # Is this role played by at most one instance, or more?
22
- attr_accessor :mandatory # In a valid fact population, is this role required to be played?
23
- attr_accessor :value_constraint # Counterpart Instances playing this role must meet this constraint
24
- attr_reader :is_identifying # Is this an identifying role for owner?
17
+ attr_reader :object_type # The ObjectType to which this role belongs
18
+ attr_reader :name # The name of the role (a Symbol)
19
+ attr_accessor :counterpart # All roles except unaries have a counterpart Role
20
+ attr_reader :unique # Is this role played by at most one instance, or more?
21
+ attr_reader :mandatory # In a valid fact population, is this role required to be played?
22
+ attr_reader :value_constraint # Counterpart Instances playing this role must meet this constraint
23
+ attr_reader :is_identifying # Is this an identifying role for object_type?
25
24
 
26
- def initialize(owner, counterpart_object_type, counterpart, name, mandatory = false, unique = true)
27
- @owner = owner
28
- @counterpart_object_type = counterpart_object_type
25
+ def initialize(object_type, counterpart, name, mandatory = false, unique = true)
26
+ @object_type = object_type
29
27
  @counterpart = counterpart
30
28
  @name = name
31
29
  @mandatory = mandatory
32
30
  @unique = unique
33
- @is_identifying = @owner.is_entity_type && @owner.identifying_role_names.include?(@name)
31
+ @is_identifying = @object_type.is_entity_type && @object_type.identifying_role_names.include?(@name)
32
+ associate_role(@object_type)
33
+ end
34
+
35
+ # Return the name of the getter method
36
+ def getter
37
+ @getter ||= @name.to_sym
38
+ end
39
+
40
+ # Return the name of the setter method
41
+ def setter
42
+ @setter ||= :"#{@name}="
43
+ end
44
+
45
+ # Return the name of the instance variable
46
+ def variable
47
+ @variable ||= "@#{@name}"
34
48
  end
35
49
 
36
50
  # Is this role a unary (created by maybe)? If so, it has no counterpart
@@ -39,34 +53,57 @@ module ActiveFacts
39
53
  counterpart == nil
40
54
  end
41
55
 
42
- def resolve_counterpart(vocabulary) #:nodoc:
43
- return @counterpart_object_type if @counterpart_object_type.is_a?(Class) # Done already
44
- klass = vocabulary.object_type(@counterpart_object_type) # Trigger the binding
45
- raise "Cannot resolve role counterpart_object_type #{@counterpart_object_type.inspect} for role #{name} in vocabulary #{vocabulary.basename}; still forward-declared?" unless klass
46
- @counterpart_object_type = klass # Memoize a successful result
56
+ def counterpart_object_type
57
+ # This method is sometimes used when unaries are used in an entity's identifier.
58
+ counterpart == nil ? TrueClass : counterpart.object_type
59
+ end
60
+
61
+ def inspect
62
+ "<Role #{object_type.name}.#{name}>"
47
63
  end
48
64
 
49
65
  def adapt(constellation, value) #:nodoc:
50
66
  # If the value is a compatible class, use it (if in another constellation, clone it),
51
67
  # else create a compatible object using the value as constructor parameters.
52
- if value.is_a?(@counterpart_object_type) # REVISIT: may be a non-primary subtype of counterpart_object_type
53
- value = value.__getobj__ if RoleProxy === value
68
+ if value.is_a?(counterpart.object_type)
54
69
  # Check that the value is in a compatible constellation, clone if not:
55
70
  if constellation && (vc = value.constellation) && vc != constellation
56
- value = value.clone # REVISIT: There's sure to be things we should reset/clone here, like non-identifying roles
71
+ # Cross-constellation assignment!
72
+ # Just take the identifying_role_values to make a new object
73
+ value = constellation.send(value.class.basename, value.identifying_role_values)
57
74
  end
58
75
  value.constellation = constellation if constellation
59
76
  else
60
77
  value = [value] unless Array === value
61
- raise "No parameters were provided to identify an #{@counterpart_object_type.basename} instance" if value == []
78
+ raise "No parameters were provided to identify an #{counterpart.object_type.basename} instance" if value == []
62
79
  if constellation
63
- value = constellation.send(@counterpart_object_type.basename.to_sym, *value)
80
+ value = constellation.send(counterpart.object_type.basename.to_sym, *value)
64
81
  else
65
- value = @counterpart_object_type.new(*value)
82
+ #trace :assert, "Constructing new #{counterpart.object_type} with #{value.inspect}" do
83
+ value = counterpart.object_type.new(*value)
84
+ #end
66
85
  end
67
86
  end
68
87
  value
69
88
  end
89
+
90
+ private
91
+ # Create a class method to access the Role object.
92
+ # This seems to add *significantly* to the runtime of the tests,
93
+ # but it's load-time, not execution-time, so it's staying!
94
+ def associate_role(klass)
95
+ role = self
96
+ klass.class_eval do
97
+ role_accessor_name = "#{role.name}_role"
98
+ unless (method(role_accessor_name) rescue nil)
99
+ (class << self; self; end).
100
+ send(:define_method, role_accessor_name) do
101
+ role
102
+ end
103
+ # else we can't create such a method without creating mayhem, so don't.
104
+ end
105
+ end
106
+ end
70
107
  end
71
108
 
72
109
  # Every ObjectType has a Role collection
@@ -14,7 +14,6 @@ module ActiveFacts
14
14
  class RoleValues #:nodoc:
15
15
  include Enumerable
16
16
 
17
- #=begin
18
17
  def initialize
19
18
  @a = []
20
19
  end
@@ -48,68 +47,11 @@ module ActiveFacts
48
47
  def update(old, value)
49
48
  @a.delete(old) if old
50
49
  @a << value if value
51
- raise "Adding RoleProxy to RoleValues collection" if value && RoleProxy === value
52
50
  end
53
51
 
54
52
  def verbalise
55
53
  "["+@a.to_a.map{|e| e.verbalise}*", "+"]"
56
54
  end
57
- #=end
58
-
59
- =begin
60
- def initialize
61
- @h = {}
62
- end
63
-
64
- def each &b
65
- @h.keys.each &b
66
- end
67
-
68
- def size
69
- @h.size
70
- end
71
-
72
- def empty?
73
- @h.size == 0
74
- end
75
-
76
- def +(a)
77
- @h.keys.+(a.is_a?(RoleValues) ? Array(a) : a)
78
- end
79
-
80
- def -(a)
81
- @h.keys - a
82
- end
83
-
84
- def single
85
- @h.size > 1 ? nil : @h.keys[0]
86
- end
87
-
88
- def update(old, value)
89
- if old
90
- unless @h.delete(old)
91
- @h.each { |k, v|
92
- next if k != old
93
- puts "#{@h.object_id}: Didn't delete #{k.verbalise} (hash=#{k.hash}) matching #{old.verbalise} (hash=#{old.hash})"
94
- puts "They are #{k.eql?(old) ? "" : "not "}eql?"
95
- found = @h[k]
96
- puts "found #{found.inspect}" if found
97
- debugger
98
- x = k.eql?(old)
99
- y = old.eql?(k)
100
- p y
101
- }
102
- raise "failed to delete #{old.verbalise}, have #{map{|e| e.verbalise}.inspect}"
103
- end
104
- end
105
- puts "#{@h.object_id}: Adding #{value.inspect}" if value && (value.name == 'Meetingisboardmeeting' rescue false)
106
- @h[value] = true if value
107
- end
108
-
109
- def verbalise
110
- "["+@h.keys.map{|e| e.verbalise}*", "+"]"
111
- end
112
- =end
113
55
 
114
56
  end
115
57
 
@@ -15,8 +15,7 @@ module ActiveFacts
15
15
  module ValueClass #:nodoc:
16
16
  def value_type *args, &block #:nodoc:
17
17
  include ActiveFacts::API::Value
18
- # the included method adds the Value::ClassMethods
19
- initialise_value_type(*args, &block)
18
+ value_type(*args, &block)
20
19
  end
21
20
  end
22
21
  end
@@ -33,19 +32,29 @@ ValueClasses.each{|c|
33
32
 
34
33
  class TrueClass #:nodoc:
35
34
  def verbalise(role_name = nil); role_name ? "#{role_name}: true" : "true"; end
35
+ def identifying_role_values; self; end
36
+ def self.identifying_role_values(*a); true; end
37
+ end
38
+
39
+ class FalseClass #:nodoc:
40
+ def verbalise(role_name = nil); role_name ? "#{role_name}: false" : "false"; end
41
+ def identifying_role_values; self; end
42
+ def self.identifying_role_values(*a); false; end
36
43
  end
37
44
 
38
45
  class NilClass #:nodoc:
39
46
  def verbalise; "nil"; end
47
+ def identifying_role_values; self; end
48
+ def self.identifying_role_values(*a); nil; end
40
49
  end
41
50
 
42
51
  class Class
43
52
  # Make this Class into a ObjectType and if necessary its module into a Vocabulary.
44
53
  # The parameters are the names (Symbols) of the identifying roles.
45
- def identified_by *args
54
+ def identified_by *args, &b
46
55
  raise "#{basename} is not an entity type" if respond_to? :value_type # Don't make a ValueType into an EntityType
47
56
  include ActiveFacts::API::Entity
48
- initialise_entity_type(*args)
57
+ identified_by(*args, &b)
49
58
  end
50
59
 
51
60
  def is_entity_type
@@ -58,7 +67,7 @@ class Decimal < BigDecimal #:nodoc:
58
67
  extend ActiveFacts::API::ValueClass
59
68
  # The problem here is you can't pass a BigDecimal to BigDecimal.new. Fix it.
60
69
  def self.new(v)
61
- if v.is_a?(BigDecimal)
70
+ if v.is_a?(BigDecimal) || v.is_a?(Bignum)
62
71
  super(v.to_s)
63
72
  else
64
73
  super
@@ -15,8 +15,14 @@ module ActiveFacts
15
15
 
16
16
  # Value instance methods:
17
17
  def initialize(*args) #:nodoc:
18
- args[0] = args[0].__getobj__ if RoleProxy === args[0]
18
+ hash = args[-1].is_a?(Hash) ? args.pop.clone : nil
19
+
19
20
  super(args)
21
+
22
+ (hash ? hash.entries : []).each do |role_name, value|
23
+ role = self.class.roles(role_name)
24
+ send(role.setter, value)
25
+ end
20
26
  end
21
27
 
22
28
  # verbalise this Value
@@ -24,18 +30,22 @@ module ActiveFacts
24
30
  "#{role_name || self.class.basename} '#{to_s}'"
25
31
  end
26
32
 
27
- # A value is its own key
33
+ # A value is its own key, unless it's a delegate for a raw value
28
34
  def identifying_role_values #:nodoc:
29
- self
35
+ __getobj__ rescue self
30
36
  end
31
37
 
32
38
  # All ValueType classes include the methods defined here
33
39
  module ClassMethods
34
40
  include Instance::ClassMethods
35
41
 
36
- def initialise_value_type *args, &block #:nodoc:
42
+ def value_type *args, &block #:nodoc:
37
43
  # REVISIT: args could be a hash, with keys :length, :scale, :unit, :allow
38
- #raise "value_type args unexpected" if args.size > 0
44
+ options = (args[-1].is_a?(Hash) ? args.pop : {})
45
+ options.each do |key, value|
46
+ raise "unknown value type option #{key}" unless respond_to?(key)
47
+ send(key, value)
48
+ end
39
49
  end
40
50
 
41
51
  class_eval do
@@ -67,12 +77,10 @@ module ActiveFacts
67
77
 
68
78
  def identifying_role_values(*args) #:nodoc:
69
79
  # If the single arg is the correct class or a subclass, use it directly
70
- #puts "#{basename}.identifying_role_values#{args.inspect}"
71
- if (args.size == 1 and (arg = args[0]).is_a?(self.class)) # No secondary supertypes allowed for value types
72
- arg = arg.__getobj__ if RoleProxy === arg
73
- return arg
80
+ if (args.size == 1 and (arg = args[0]).is_a?(self)) # No secondary supertypes allowed for value types
81
+ return arg.identifying_role_values
74
82
  end
75
- new(*args)
83
+ new(*args).identifying_role_values
76
84
  end
77
85
 
78
86
  def assert_instance(constellation, args) #:nodoc:
@@ -80,25 +88,24 @@ module ActiveFacts
80
88
  # The key of an instance is the value or array of keys of the identifying values.
81
89
  # The key values aren't necessarily present in the constellation, even after this.
82
90
  key = identifying_role_values(*args)
83
- #puts "#{klass} key is #{key.inspect}"
84
91
 
85
92
  # Find and return an existing instance matching this key
86
93
  instances = constellation.instances[self] # All instances of this class in this constellation
87
94
  instance = instances[key]
88
- # DEBUG: puts "assert #{self.basename} #{key.inspect} #{instance ? "exists" : "new"}"
89
95
  return instance, key if instance # A matching instance of this class
90
96
 
91
- instance = new(*args)
97
+ #trace :assert, "Constructing new #{self} with #{args.inspect}" do
98
+ instance = new(*args)
99
+ #end
92
100
 
93
101
  instance.constellation = constellation
94
102
  return *index_instance(instance)
95
103
  end
96
104
 
97
- def index_instance(instance, key = nil) #:nodoc:
105
+ def index_instance(instance, key = nil, key_roles = nil) #:nodoc:
98
106
  instances = instance.constellation.instances[self]
99
107
  key = instance.identifying_role_values
100
108
  instances[key] = instance
101
- # DEBUG: puts "indexing value #{basename} using #{key.inspect} in #{constellation.object_id}"
102
109
 
103
110
  # Index the instance for each supertype:
104
111
  supertypes.each do |supertype|
@@ -109,7 +116,6 @@ module ActiveFacts
109
116
  end
110
117
 
111
118
  def inherited(other) #:nodoc:
112
- #puts "REVISIT: ValueType #{self} < #{self.superclass} was inherited by #{other}; not implemented" #+"from #{caller*"\n\t"}"
113
119
  # Copy the type parameters here, etc?
114
120
  other.send :realise_supertypes, self
115
121
  vocabulary.__add_object_type(other)
@@ -120,11 +126,8 @@ module ActiveFacts
120
126
  def self.included other #:nodoc:
121
127
  other.send :extend, ClassMethods
122
128
 
123
- #puts "ValueType included in #{other.basename} from #{caller*"\n\t"}"
124
-
125
129
  # Register ourselves with the parent module, which has become a Vocabulary:
126
130
  vocabulary = other.modspace
127
- # puts "ValueType.included(#{other.inspect})"
128
131
  unless vocabulary.respond_to? :object_type # Extend module with Vocabulary if necessary
129
132
  vocabulary.send :extend, Vocabulary
130
133
  end
@@ -14,21 +14,28 @@ module ActiveFacts
14
14
  # and can resolve the forward references when the class is finally defined.
15
15
  # Construction of a Constellation requires a Vocabuary as argument.
16
16
  module Vocabulary
17
- # With a parameter, look up a object_type class by name.
17
+ # With a parameter, look up an object type by name.
18
18
  # Without, return the hash (keyed by the class' basename) of all object_types in this vocabulary
19
19
  def object_type(name = nil)
20
20
  @object_type ||= {}
21
21
  return @object_type unless name
22
22
 
23
- return name if name.is_a? Class
23
+ if name.is_a? Class
24
+ raise "#{name} must be an object type in #{self.name}" unless name.vocabulary == self
25
+ return name
26
+ end
24
27
 
25
- # puts "Looking up object_type #{name} in #{self.name}"
26
28
  camel = name.to_s.camelcase
27
29
  if (c = @object_type[camel])
28
30
  __bind(camel)
29
- return c
31
+ c
32
+ else
33
+ begin
34
+ const_get("#{self.name}::#{camel}")
35
+ rescue NameError
36
+ nil
37
+ end
30
38
  end
31
- return (const_get("#{name}::#{camel}") rescue nil)
32
39
  end
33
40
 
34
41
  # Create a new constellation over this vocabulary
@@ -52,13 +59,11 @@ module ActiveFacts
52
59
  def __add_object_type(klass) #:nodoc:
53
60
  name = klass.basename
54
61
  __bind(name)
55
- # puts "Adding object_type #{name} to #{self.name}"
56
62
  @object_type ||= {}
57
63
  @object_type[klass.basename] = klass
58
64
  end
59
65
 
60
66
  def __delay(object_type_name, args, &block) #:nodoc:
61
- # puts "Arranging for delayed binding on #{object_type_name.inspect}"
62
67
  @delayed ||= Hash.new { |h,k| h[k] = [] }
63
68
  @delayed[object_type_name] << [args, block]
64
69
  end
@@ -66,9 +71,7 @@ module ActiveFacts
66
71
  # __bind raises an error if the named class doesn't exist yet.
67
72
  def __bind(object_type_name) #:nodoc:
68
73
  object_type = const_get(object_type_name)
69
- # puts "#{name}.__bind #{object_type_name} -> #{object_type.name}" if object_type
70
74
  if (@delayed && @delayed.include?(object_type_name))
71
- # $stderr.puts "#{object_type_name} was delayed, binding now"
72
75
  d = @delayed[object_type_name]
73
76
  d.each{|(a,b)|
74
77
  b.call(object_type, *a)
@@ -0,0 +1,109 @@
1
+ #
2
+ # ActiveFacts Support code.
3
+ # The trace method supports indented tracing.
4
+ # Set the TRACE environment variable to enable it. Search the code to find the TRACE keywords, or use "all".
5
+ #
6
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
7
+ #
8
+ module ActiveFacts
9
+ (class << self; self; end).class_eval do
10
+ attr_accessor :tracer
11
+ end
12
+
13
+ class Tracer
14
+ def initialize
15
+ @nested = false # Set when a block enables all enclosed tracing
16
+ @available = {}
17
+
18
+ # First time, initialise the tracing environment
19
+ @indent = 0
20
+ @keys = {}
21
+ if (e = ENV["TRACE"])
22
+ e.split(/[^_a-zA-Z0-9]/).each{|k| enable(k) }
23
+ if @keys[:help]
24
+ at_exit {
25
+ @stderr.puts "---\nDebugging keys available: #{@available.keys.map{|s| s.to_s}.sort*", "}"
26
+ }
27
+ end
28
+ require 'ruby-debug' if @keys[:debug]
29
+ end
30
+ end
31
+
32
+ def keys
33
+ @available.keys
34
+ end
35
+
36
+ def enabled key
37
+ !key.empty? && @keys[key.to_sym]
38
+ end
39
+
40
+ def enable key
41
+ !key.to_s.empty? && @keys[key.to_sym] = true
42
+ end
43
+
44
+ def disable key
45
+ !key.to_s.empty? && @keys.delete(key.to_sym)
46
+ end
47
+
48
+ def toggle key
49
+ !key.to_s.empty? and enabled(key) ? (disable(key); false) : (enable(key); true)
50
+ end
51
+
52
+ def selected(args)
53
+ # Figure out whether this trace is enabled (itself or by :all), if it nests, and if we should print the key:
54
+ key =
55
+ if Symbol === args[0]
56
+ control = args.shift
57
+ if (s = control.to_s) =~ /_\Z/
58
+ nested = true
59
+ s.sub(/_\Z/, '').to_sym # Avoid creating new strings willy-nilly
60
+ else
61
+ control
62
+ end
63
+ else
64
+ :all
65
+ end
66
+
67
+ @available[key] ||= key # Remember that this trace was requested, for help
68
+ enabled = @nested || # This trace is enabled because it's in a nested block
69
+ @keys[key] || # This trace is enabled in its own right
70
+ @keys[:all] # This trace is enabled because all are
71
+ @nested = nested
72
+ [
73
+ (enabled ? 1 : 0),
74
+ @keys[:all] ? " %-15s"%control : nil
75
+ ]
76
+ end
77
+
78
+ def show(*args)
79
+ enabled, key_to_show = selected(args)
80
+
81
+ # Emit the message if enabled or a parent is:
82
+ if args.size > 0 && enabled == 1
83
+ puts "\##{key_to_show} " +
84
+ ' '*@indent +
85
+ args.
86
+ # A laudable aim, certainly, but in practise the Procs leak and slow things down:
87
+ # map{|a| a.respond_to?(:call) ? a.call : a}.
88
+ join(' ')
89
+ end
90
+ @indent += enabled
91
+ enabled
92
+ end
93
+
94
+ def trace(*args, &block)
95
+ begin
96
+ old_indent, old_nested, enabled = @indent, @nested, show(*args)
97
+ return (block || proc { enabled == 1 }).call
98
+ ensure
99
+ @indent, @nested = old_indent, old_nested
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ class Object
106
+ def trace *args, &block
107
+ (ActiveFacts.tracer ||= ActiveFacts::Tracer.new).trace(*args, &block)
108
+ end
109
+ end