activefacts-api 0.8.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,80 @@
1
+ #
2
+ # ActiveFacts API
3
+ # Role class.
4
+ # Each accessor method created on an instance corresponds to a Role object in the instance's class.
5
+ # Binary fact types construct a Role at each end.
6
+ #
7
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
8
+ #
9
+ module ActiveFacts
10
+ module API
11
+
12
+ # A Role represents the relationship of one object to another (or to a boolean condition).
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.
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?
25
+
26
+ def initialize(owner, counterpart_object_type, counterpart, name, mandatory = false, unique = true)
27
+ @owner = owner
28
+ @counterpart_object_type = counterpart_object_type
29
+ @counterpart = counterpart
30
+ @name = name
31
+ @mandatory = mandatory
32
+ @unique = unique
33
+ @is_identifying = @owner.is_entity_type && @owner.identifying_role_names.include?(@name)
34
+ end
35
+
36
+ # Is this role a unary (created by maybe)? If so, it has no counterpart
37
+ def unary?
38
+ # N.B. A role with a forward reference looks unary until it is resolved.
39
+ counterpart == nil
40
+ end
41
+
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
47
+ end
48
+
49
+ def adapt(constellation, value) #:nodoc:
50
+ # If the value is a compatible class, use it (if in another constellation, clone it),
51
+ # 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
54
+ # Check that the value is in a compatible constellation, clone if not:
55
+ 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
57
+ end
58
+ value.constellation = constellation if constellation
59
+ else
60
+ value = [value] unless Array === value
61
+ raise "No parameters were provided to identify an #{@counterpart_object_type.basename} instance" if value == []
62
+ if constellation
63
+ value = constellation.send(@counterpart_object_type.basename.to_sym, *value)
64
+ else
65
+ value = @counterpart_object_type.new(*value)
66
+ end
67
+ end
68
+ value
69
+ end
70
+ end
71
+
72
+ # Every ObjectType has a Role collection
73
+ # REVISIT: You can enumerate the object_type's own roles, or inherited roles as well.
74
+ class RoleCollection < Hash #:nodoc:
75
+ def verbalise
76
+ keys.sort_by(&:to_s).inspect
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,71 @@
1
+ #
2
+ # ActiveFacts Runtime API
3
+ # RoleProxy class, still somewhat experimental
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ require 'delegate'
8
+
9
+ module ActiveFacts
10
+ module API
11
+ #
12
+ # When you use the accessor method created by has_one, one_to_one, or maybe, you get a RoleProxy for the actual value.
13
+ # This behaves almost exactly as the value, but it knows through which role you fetched it.
14
+ # That will allow it to verbalise itself using the correct reading for that role.
15
+ #
16
+ # Don't use "SomeClass === role_value" to test the type, use "role_value.is_a?(SomeClass)" instead.
17
+ #
18
+ # In future, retrieving a value by indexing into a RoleValues array will do the same thing.
19
+ #
20
+ class RoleProxy < SimpleDelegator
21
+ def initialize(role, o = nil) #:nodoc:
22
+ @role = role # REVISIT: Use this to implement verbalise()
23
+ __setobj__(o)
24
+ end
25
+
26
+ def method_missing(m, *a, &b) #:nodoc:
27
+ begin
28
+ super # Delegate first
29
+ rescue NoMethodError => e
30
+ __getobj__.method_missing(m, *a, &b)
31
+ rescue => e
32
+ raise
33
+ end
34
+ end
35
+
36
+ def class #:nodoc:
37
+ __getobj__.class
38
+ end
39
+
40
+ def is_a? klass #:nodoc:
41
+ __getobj__.is_a? klass
42
+ end
43
+
44
+ def to_s #:nodoc:
45
+ __getobj__.to_s
46
+ end
47
+
48
+ # This is strongly deprecated, and omitting it doesn't seem to hurt:
49
+ #def object_id #:nodoc:
50
+ # __getobj__.object_id
51
+ #end
52
+
53
+ # REVISIT: Should Proxies hash and eql? the same as their wards?
54
+ def hash #:nodoc:
55
+ __getobj__.hash ^ self.class.hash
56
+ end
57
+
58
+ def eql?(o) #:nodoc:
59
+ self.class == o.class and __getobj__.eql?(o)
60
+ end
61
+
62
+ def ==(o) #:nodoc:
63
+ __getobj__.==(o)
64
+ end
65
+
66
+ def inspect #:nodoc:
67
+ "Proxy:#{__getobj__.inspect}"
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,117 @@
1
+ #
2
+ # ActiveFacts Runtime API
3
+ # RoleValues, manages the set of instances involved in a many_to_one relationship.
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
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
+ #
11
+ module ActiveFacts
12
+ module API
13
+
14
+ class RoleValues #:nodoc:
15
+ include Enumerable
16
+
17
+ #=begin
18
+ def initialize
19
+ @a = []
20
+ end
21
+
22
+ def each &b
23
+ # REVISIT: Provide a configuration variable to enable this heckling during testing:
24
+ #@a.sort_by{rand}.each &b
25
+ @a.each &b
26
+ end
27
+
28
+ def size
29
+ @a.size
30
+ end
31
+
32
+ def empty?
33
+ @a.size == 0
34
+ end
35
+
36
+ def +(a)
37
+ @a.+(a.is_a?(RoleValues) ? Array(a) : a)
38
+ end
39
+
40
+ def -(a)
41
+ @a - a
42
+ end
43
+
44
+ def single
45
+ @a.size > 1 ? nil : @a[0]
46
+ end
47
+
48
+ def update(old, value)
49
+ @a.delete(old) if old
50
+ @a << value if value
51
+ raise "Adding RoleProxy to RoleValues collection" if value && RoleProxy === value
52
+ end
53
+
54
+ def verbalise
55
+ "["+@a.to_a.map{|e| e.verbalise}*", "+"]"
56
+ 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
+
114
+ end
115
+
116
+ end
117
+ end
@@ -0,0 +1,87 @@
1
+ #
2
+ # ActiveFacts Runtime API
3
+ # Standard types extensions.
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ # These extensions add ActiveFacts ObjectType and Instance behaviour into base Ruby value classes,
8
+ # and allow any Class to become an Entity.
9
+ #
10
+ require 'date'
11
+
12
+ module ActiveFacts
13
+ module API
14
+ # Adapter module to add value_type to all potential value classes
15
+ module ValueClass #:nodoc:
16
+ def value_type *args, &block #:nodoc:
17
+ include ActiveFacts::API::Value
18
+ # the included method adds the Value::ClassMethods
19
+ initialise_value_type(*args, &block)
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ require 'activefacts/api/numeric'
26
+
27
+ # Add the methods that convert our classes into ObjectType types:
28
+
29
+ ValueClasses = [String, Date, DateTime, Time, Int, Real, AutoCounter]
30
+ ValueClasses.each{|c|
31
+ c.send :extend, ActiveFacts::API::ValueClass
32
+ }
33
+
34
+ class TrueClass #:nodoc:
35
+ def verbalise(role_name = nil); role_name ? "#{role_name}: true" : "true"; end
36
+ end
37
+
38
+ class NilClass #:nodoc:
39
+ def verbalise; "nil"; end
40
+ end
41
+
42
+ class Class
43
+ # Make this Class into a ObjectType and if necessary its module into a Vocabulary.
44
+ # The parameters are the names (Symbols) of the identifying roles.
45
+ def identified_by *args
46
+ raise "#{basename} is not an entity type" if respond_to? :value_type # Don't make a ValueType into an EntityType
47
+ include ActiveFacts::API::Entity
48
+ initialise_entity_type(*args)
49
+ end
50
+
51
+ def is_entity_type
52
+ respond_to?(:identifying_role_names)
53
+ end
54
+ end
55
+
56
+ require 'bigdecimal'
57
+ class Decimal < BigDecimal #:nodoc:
58
+ extend ActiveFacts::API::ValueClass
59
+ # The problem here is you can't pass a BigDecimal to BigDecimal.new. Fix it.
60
+ def self.new(v)
61
+ if v.is_a?(BigDecimal)
62
+ super(v.to_s)
63
+ else
64
+ super
65
+ end
66
+ end
67
+ end
68
+
69
+ # These types are generated on conversion from NORMA's types:
70
+ class Char < String #:nodoc: # FixedLengthText
71
+ end
72
+ class Text < String #:nodoc: # LargeLengthText
73
+ end
74
+ class Image < String #:nodoc: # PictureRawData
75
+ end
76
+ class SignedInteger < Int #:nodoc:
77
+ end
78
+ class UnsignedInteger < Int #:nodoc:
79
+ end
80
+ class AutoTimeStamp < String #:nodoc: AutoTimeStamp
81
+ end
82
+ class Blob < String #:nodoc: VariableLengthRawData
83
+ end
84
+ unless Object.const_defined?("Money")
85
+ class Money < Decimal #:nodoc:
86
+ end
87
+ end
@@ -0,0 +1,66 @@
1
+ #
2
+ # ActiveFacts Runtime API.
3
+ # Various additions or patches to Ruby built-in classes, and some global support methods
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+
8
+ class Symbol #:nodoc:
9
+ def to_proc
10
+ Proc.new{|*args| args.shift.__send__(self, *args)}
11
+ end
12
+ end
13
+
14
+ class String #:nodoc:
15
+ # This may be overridden by a version from ActiveSupport. For our purposes, either will work.
16
+ def camelcase(first_letter = :upper)
17
+ if first_letter == :upper
18
+ gsub(/(^|[_\s]+)([A-Za-z])/){ $2.upcase }
19
+ else
20
+ gsub(/([_\s]+)([A-Za-z])/){ $2.upcase }
21
+ end
22
+ end
23
+
24
+ def snakecase
25
+ gsub(/([a-z])([A-Z])/,'\1_\2').downcase
26
+ end
27
+
28
+ def camelwords
29
+ gsub(/-([a-zA-Z])/){ $1.upcase }. # Break and upcase on hyphenated words
30
+ gsub(/([a-z])([A-Z])/,'\1_\2').
31
+ split(/[_\s]+/)
32
+ end
33
+ end
34
+
35
+ class Module #:nodoc:
36
+ def modspace
37
+ space = name[ 0...(name.rindex( '::' ) || 0)]
38
+ space == '' ? Object : eval(space)
39
+ end
40
+
41
+ def basename
42
+ name.gsub(/.*::/, '')
43
+ end
44
+ end
45
+
46
+ module ActiveFacts #:nodoc:
47
+ # If the args array ends with a hash, remove it.
48
+ # If the remaining args are fewer than the arg_names,
49
+ # extract values from the hash and append them to args.
50
+ # Return the new args array and the hash.
51
+ # In any case leave the original args unmodified.
52
+ def self.extract_hash_args(arg_names, args)
53
+ if Hash === args[-1]
54
+ arg_hash = args[-1] # Don't pop args, leave it unmodified
55
+ args = args[0..-2]
56
+ arg_hash = arg_hash.clone if (args.size < arg_names.size)
57
+ while args.size < arg_names.size
58
+ args << arg_hash[n = arg_names[args.size]]
59
+ arg_hash.delete(n)
60
+ end
61
+ else
62
+ arg_hash = {}
63
+ end
64
+ return args, arg_hash
65
+ end
66
+ end
@@ -0,0 +1,135 @@
1
+ #
2
+ # ActiveFacts Runtime API
3
+ # Value module (mixins for ValueType classes and instances)
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ # The methods of this module are added to Value type classes.
8
+ #
9
+ module ActiveFacts
10
+ module API
11
+
12
+ # All Value instances include the methods defined here
13
+ module Value
14
+ include Instance
15
+
16
+ # Value instance methods:
17
+ def initialize(*args) #:nodoc:
18
+ args[0] = args[0].__getobj__ if RoleProxy === args[0]
19
+ super(args)
20
+ end
21
+
22
+ # verbalise this Value
23
+ def verbalise(role_name = nil)
24
+ "#{role_name || self.class.basename} '#{to_s}'"
25
+ end
26
+
27
+ # A value is its own key
28
+ def identifying_role_values #:nodoc:
29
+ self
30
+ end
31
+
32
+ # All ValueType classes include the methods defined here
33
+ module ClassMethods
34
+ include Instance::ClassMethods
35
+
36
+ def initialise_value_type *args, &block #:nodoc:
37
+ # REVISIT: args could be a hash, with keys :length, :scale, :unit, :allow
38
+ #raise "value_type args unexpected" if args.size > 0
39
+ end
40
+
41
+ class_eval do
42
+ define_method :length do |*args|
43
+ @length = args[0] if args.length > 0
44
+ @length
45
+ end
46
+ end
47
+
48
+ class_eval do
49
+ define_method :scale do |*args|
50
+ @scale = args[0] if args.length > 0
51
+ @scale
52
+ end
53
+ end
54
+
55
+ class_eval do
56
+ define_method :restrict do |*value_ranges|
57
+ @value_ranges = *value_ranges
58
+ end
59
+ end
60
+
61
+ # verbalise this ValueType
62
+ def verbalise
63
+ # REVISIT: Add length and scale here, if set
64
+ # REVISIT: Set vocabulary name of superclass if not same as this
65
+ "#{basename} = #{superclass.basename}();"
66
+ end
67
+
68
+ def identifying_role_values(*args) #:nodoc:
69
+ # 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
74
+ end
75
+ new(*args)
76
+ end
77
+
78
+ def assert_instance(constellation, args) #:nodoc:
79
+ # Build the key for this instance from the args
80
+ # The key of an instance is the value or array of keys of the identifying values.
81
+ # The key values aren't necessarily present in the constellation, even after this.
82
+ key = identifying_role_values(*args)
83
+ #puts "#{klass} key is #{key.inspect}"
84
+
85
+ # Find and return an existing instance matching this key
86
+ instances = constellation.instances[self] # All instances of this class in this constellation
87
+ instance = instances[key]
88
+ # DEBUG: puts "assert #{self.basename} #{key.inspect} #{instance ? "exists" : "new"}"
89
+ return instance, key if instance # A matching instance of this class
90
+
91
+ instance = new(*args)
92
+
93
+ instance.constellation = constellation
94
+ return *index_instance(instance)
95
+ end
96
+
97
+ def index_instance(instance, key = nil) #:nodoc:
98
+ instances = instance.constellation.instances[self]
99
+ key = instance.identifying_role_values
100
+ instances[key] = instance
101
+ # DEBUG: puts "indexing value #{basename} using #{key.inspect} in #{constellation.object_id}"
102
+
103
+ # Index the instance for each supertype:
104
+ supertypes.each do |supertype|
105
+ supertype.index_instance(instance, key)
106
+ end
107
+
108
+ return instance, key
109
+ end
110
+
111
+ def inherited(other) #:nodoc:
112
+ #puts "REVISIT: ValueType #{self} < #{self.superclass} was inherited by #{other}; not implemented" #+"from #{caller*"\n\t"}"
113
+ # Copy the type parameters here, etc?
114
+ other.send :realise_supertypes, self
115
+ vocabulary.__add_object_type(other)
116
+ super
117
+ end
118
+ end
119
+
120
+ def self.included other #:nodoc:
121
+ other.send :extend, ClassMethods
122
+
123
+ #puts "ValueType included in #{other.basename} from #{caller*"\n\t"}"
124
+
125
+ # Register ourselves with the parent module, which has become a Vocabulary:
126
+ vocabulary = other.modspace
127
+ # puts "ValueType.included(#{other.inspect})"
128
+ unless vocabulary.respond_to? :object_type # Extend module with Vocabulary if necessary
129
+ vocabulary.send :extend, Vocabulary
130
+ end
131
+ vocabulary.__add_object_type(other)
132
+ end
133
+ end
134
+ end
135
+ end