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,60 @@
1
+ #
2
+ # ActiveFacts Runtime API
3
+ # Instance (mixin module for instances of a ObjectType - a class with ObjectType mixed in)
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ # Instance methods are extended into all instances, whether of value or entity types.
8
+ #
9
+ module ActiveFacts
10
+ module API
11
+ # Every Instance of a ObjectType (A Value type or an Entity type) includes the methods of this module:
12
+ module Instance
13
+ # What constellation does this Instance belong to (if any):
14
+ attr_accessor :constellation
15
+
16
+ def initialize(args = []) #:nodoc:
17
+ unless (self.class.is_entity_type)
18
+ #if (self.class.superclass != Object)
19
+ # puts "constructing #{self.class.superclass} with #{args.inspect}"
20
+ super(*args)
21
+ end
22
+ end
23
+
24
+ # Verbalise this instance
25
+ def verbalise
26
+ # This method should always be overridden in subclasses
27
+ raise "#{self.class} Instance verbalisation needed"
28
+ end
29
+
30
+ # De-assign all functional roles and remove from constellation, if any.
31
+ def retract
32
+ # Delete from the constellation first, so it can remember our identifying role values
33
+ @constellation.__retract(self) if @constellation
34
+
35
+ # Now, for all roles (from this class and all supertypes), assign nil to all functional roles
36
+ # The counterpart roles get cleared automatically.
37
+ ([self.class]+self.class.supertypes_transitive).each do |klass|
38
+ klass.roles.each do |role_name, role|
39
+ next if role.unary?
40
+ next if !role.unique
41
+
42
+ counterpart = role.counterpart
43
+ puts "Nullifying mandatory role #{role.name} of #{role.owner.name}" if counterpart.mandatory
44
+
45
+ send "#{role.name}=", nil
46
+ end
47
+ end
48
+ end
49
+
50
+ module ClassMethods #:nodoc:
51
+ include ObjectType
52
+ # Add Instance class methods here
53
+ end
54
+
55
+ def Instance.included other #:nodoc:
56
+ other.send :extend, ClassMethods
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,84 @@
1
+ #
2
+ # ActiveFacts Runtime API
3
+ # InstanceIndex class
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ module ActiveFacts
8
+ module API
9
+ #
10
+ # Each Constellation maintains an InstanceIndex for each ObjectType in its Vocabulary.
11
+ # The InstanceIndex object is returned when you call @constellation.ObjectType with no
12
+ # arguments (where ObjectType is the object_type name you're interested in)
13
+ #
14
+ class InstanceIndex
15
+ def []=(key, value) #:nodoc:
16
+ raise "Adding RoleProxy to InstanceIndex" if value && RoleProxy === value
17
+ h[key] = value
18
+ end
19
+
20
+ def [](*args)
21
+ a = args
22
+ #a = naked(args)
23
+ # p "vvvv",
24
+ # args,
25
+ # a,
26
+ # keys.map{|k| v=super(k); (RoleProxy === k ? "*" : "")+k.to_s+"=>"+(RoleProxy === v ? "*" : "")+v.to_s}*",",
27
+ # "^^^^"
28
+ h[*a]
29
+ #super(*a)
30
+ end
31
+
32
+ def size
33
+ h.size
34
+ end
35
+
36
+ def empty?
37
+ h.size == 0
38
+ end
39
+
40
+ def each &b
41
+ h.each &b
42
+ end
43
+
44
+ def map &b
45
+ h.map &b
46
+ end
47
+
48
+ def detect &b
49
+ r = h.detect &b
50
+ r ? r[1] : nil
51
+ end
52
+
53
+ # Return an array of all the instances of this object_type
54
+ def values
55
+ h.values
56
+ end
57
+
58
+ # Return an array of the identifying role values arrays for all the instances of this object_type
59
+ def keys
60
+ h.keys
61
+ end
62
+
63
+ def delete_if(&b) #:nodoc:
64
+ h.delete_if &b
65
+ end
66
+
67
+ private
68
+ def h
69
+ @hash ||= {}
70
+ end
71
+
72
+ def naked(o)
73
+ case o
74
+ when Array
75
+ o.map{|e| naked(e) }
76
+ when RoleProxy
77
+ o.__getobj__
78
+ else
79
+ o
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,175 @@
1
+ #
2
+ # ActiveFacts Runtime API
3
+ # Numeric and Date delegates and hacks to handle immediate types.
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ # These delegates are required because Integer & Float don't support new,
8
+ # and can't be sensibly subclassed. Just delegate to an instance var.
9
+ # Date and DateTime don't have a sensible new() method, so we monkey-patch one here.
10
+ #
11
+ require 'delegate'
12
+ require 'date'
13
+
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
19
+
20
+ def to_s #:nodoc:
21
+ __getobj__.to_s
22
+ end
23
+
24
+ def hash #:nodoc:
25
+ __getobj__.hash ^ self.class.hash
26
+ end
27
+
28
+ def eql?(o) #:nodoc:
29
+ self.class == o.class and __getobj__.eql?(Integer(o))
30
+ end
31
+
32
+ def ==(o) #:nodoc:
33
+ __getobj__.==(o)
34
+ end
35
+
36
+ def is_a?(k)
37
+ __getobj__.is_a?(k)
38
+ end
39
+
40
+ def inspect
41
+ "#{self.class.basename}:#{__getobj__.inspect}"
42
+ end
43
+ end
44
+
45
+ # It's not possible to subclass Float, so instead we delegate to it.
46
+ class Real < SimpleDelegator
47
+ def initialize(r = nil) #:nodoc:
48
+ __setobj__(Float(r))
49
+ end
50
+
51
+ def hash #:nodoc:
52
+ __getobj__.hash ^ self.class.hash
53
+ end
54
+
55
+ def to_s #:nodoc:
56
+ __getobj__.to_s
57
+ end
58
+
59
+ def eql?(o) #:nodoc:
60
+ self.class == o.class and __getobj__.eql?(Float(o))
61
+ end
62
+
63
+ def ==(o) #:nodoc:
64
+ __getobj__.==(o)
65
+ end
66
+
67
+ def is_a?(k)
68
+ __getobj__.is_a?(k)
69
+ end
70
+
71
+ def inspect #:nodoc:
72
+ "#{self.class.basename}:#{__getobj__.inspect}"
73
+ end
74
+ end
75
+
76
+ # A Date can be constructed from any Date subclass, not just using the normal date constructors.
77
+ class ::Date
78
+ class << self; alias_method :old_new, :new end
79
+ # Date.new cannot normally be called passing a Date as the parameter. This allows that.
80
+ def self.new(*a, &b)
81
+ #puts "Constructing date with #{a.inspect} from #{caller*"\n\t"}"
82
+ if (a.size == 1 && a[0].is_a?(Date))
83
+ a = a[0]
84
+ civil(a.year, a.month, a.day, a.start)
85
+ elsif (a.size == 1 && a[0].is_a?(String))
86
+ parse(a[0])
87
+ else
88
+ civil(*a, &b)
89
+ end
90
+ end
91
+ end
92
+
93
+ # A DateTime can be constructed from any Date or DateTime subclass
94
+ class ::DateTime
95
+ class << self; alias_method :old_new, :new end
96
+ # DateTime.new cannot normally be called passing a Date or DateTime as the parameter. This allows that.
97
+ def self.new(*a, &b)
98
+ #puts "Constructing DateTime with #{a.inspect} from #{caller*"\n\t"}"
99
+ if (a.size == 1)
100
+ a = a[0]
101
+ if (DateTime === a)
102
+ civil(a.year, a.month, a.day, a.hour, a.min, a.sec, a.start)
103
+ elsif (Date === a)
104
+ civil(a.year, a.month, a.day, a.start)
105
+ else
106
+ civil(*a, &b)
107
+ end
108
+ else
109
+ civil(*a, &b)
110
+ end
111
+ end
112
+ end
113
+
114
+ # The AutoCounter class is an integer, but only after the value
115
+ # has been established in the database.
116
+ # Construct it with the value :new to get an uncommitted value.
117
+ # You can use this new instance as a value of any role of this type, including to identify an entity instance.
118
+ # The assigned value will be filled out everywhere it needs to be, upon save.
119
+ class AutoCounter
120
+ def initialize(i = :new)
121
+ raise "AutoCounter #{self.class} may not be #{i.inspect}" unless i == :new or i.is_a?(Integer)
122
+ # puts "new AutoCounter #{self.class} from\n\t#{caller.select{|s| s !~ %r{rspec}}*"\n\t"}"
123
+ @value = i == :new ? nil : i
124
+ end
125
+
126
+ # Assign a definite value to an AutoCounter; this may only be done once
127
+ def assign(i)
128
+ raise ArgumentError if @value
129
+ @value = i.to_i
130
+ end
131
+
132
+ # Ask whether a definite value has been assigned
133
+ def defined?
134
+ !@value.nil?
135
+ end
136
+
137
+ def to_s
138
+ if self.defined?
139
+ @value.to_s
140
+ else
141
+ "new_#{object_id}"
142
+ end
143
+ end
144
+
145
+ # An AutoCounter may only be used in numeric expressions after a definite value has been assigned
146
+ def self.coerce(i)
147
+ raise ArgumentError unless @value
148
+ [ i.to_i, @value ]
149
+ end
150
+
151
+ def inspect
152
+ "\#<AutoCounter "+to_s+">"
153
+ end
154
+
155
+ def hash #:nodoc:
156
+ to_s.hash ^ self.class.hash
157
+ end
158
+
159
+ def eql?(o) #:nodoc:
160
+ self.class == o.class and to_s.eql?(o.to_s)
161
+ end
162
+
163
+ def self.inherited(other) #:nodoc:
164
+ def other.identifying_role_values(*args)
165
+ return nil if args == [:new] # A new object has no identifying_role_values
166
+ return new(*args)
167
+ end
168
+ super
169
+ end
170
+
171
+ private
172
+ def clone
173
+ raise "Not allowed to clone AutoCounters"
174
+ end
175
+ end
@@ -0,0 +1,411 @@
1
+ #
2
+ # ActiveFacts Runtime API
3
+ # ObjectType (a mixin module for the class Class)
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+
8
+ module ActiveFacts
9
+ module API
10
+ module Vocabulary; end
11
+
12
+ # ObjectType contains methods that are added as class methods to all Value and Entity classes.
13
+ module ObjectType
14
+ # What vocabulary (Ruby module) does this object_type belong to?
15
+ def vocabulary
16
+ modspace # The module that contains this object_type.
17
+ end
18
+
19
+ # Each ObjectType maintains a list of the Roles it plays:
20
+ def roles(name = nil)
21
+ unless instance_variable_defined? "@roles"
22
+ @roles = RoleCollection.new # Initialize and extend without warnings.
23
+ end
24
+ case name
25
+ when nil
26
+ @roles
27
+ when Symbol, String
28
+ # Search this class then all supertypes:
29
+ unless role = @roles[name.to_sym]
30
+ role = nil
31
+ supertypes.each do |supertype|
32
+ r = supertype.roles(name) rescue nil
33
+ next unless r
34
+ role = r
35
+ break
36
+ end
37
+ end
38
+ raise "Role #{basename}.#{name} is not defined" unless role
39
+ # Bind the role if possible, but don't require it:
40
+ role.resolve_counterpart(vocabulary) rescue nil unless role.counterpart_object_type.is_a?(Class)
41
+ role
42
+ else
43
+ nil
44
+ end
45
+ end
46
+
47
+ # Define a unary fact type attached to this object_type; in essence, a boolean attribute.
48
+ #
49
+ # Example: maybe :is_ceo
50
+ def maybe(role_name)
51
+ realise_role(roles[role_name] = Role.new(self, TrueClass, nil, role_name))
52
+ end
53
+
54
+ # Define a binary fact type relating this object_type to another,
55
+ # with a uniqueness constraint only on this object_type's role.
56
+ # This method creates two accessor methods, one in this object_type and one in the other object_type.
57
+ # * role_name is a Symbol for the name of the role (this end of the relationship)
58
+ # Options contain optional keys:
59
+ # * :class - A class name, Symbol or String naming a class, required if it doesn't match the role_name. Use a symbol or string if the class isn't defined yet, and the methods will be created later, when the class is first defined.
60
+ # * :mandatory - if this role may not be NULL in a valid fact population, say :mandatory => true. Mandatory constraints are only enforced during validation (e.g. before saving).
61
+ # * :counterpart - use if the role at the other end should have a name other than the default :all_<object_type> or :all_<object_type>\_as_<role_name>
62
+ # * :reading - for verbalisation. Not used yet.
63
+ # * :restrict - a list of values or ranges which this role may take. Not used yet.
64
+ def has_one(role_name, options = {})
65
+ role_name, related, mandatory, related_role_name = extract_binary_params(false, role_name, options)
66
+ define_binary_fact_type(false, role_name, related, mandatory, related_role_name)
67
+ end
68
+
69
+ # Define a binary fact type joining this object_type to another,
70
+ # with uniqueness constraints in both directions, i.e. a one-to-one relationship
71
+ # This method creates two accessor methods, one in this object_type and one in the other object_type.
72
+ # * role_name is a Symbol for the name of the role (this end of the relationship)
73
+ # Options contain optional keys:
74
+ # * :class - A class name, Symbol or String naming a class, required if it doesn't match the role_name. Use a symbol or string if the class isn't defined yet, and the methods will be created later, when the class is first defined.
75
+ # * :mandatory - if this role may not be NULL in a valid fact population, say :mandatory => true. Mandatory constraints are only enforced during validation (e.g. before saving).
76
+ # * :counterpart - use if the role at the other end should have a name other than the default :all_<object_type> or :all_<object_type>\_as_<role_name>
77
+ # * :reading - for verbalisation. Not used yet.
78
+ # * :restrict - a list of values or ranges which this role may take. Not used yet.
79
+ def one_to_one(role_name, options = {})
80
+ role_name, related, mandatory, related_role_name =
81
+ extract_binary_params(true, role_name, options)
82
+ define_binary_fact_type(true, role_name, related, mandatory, related_role_name)
83
+ end
84
+
85
+ # Access supertypes or add new supertypes; multiple inheritance.
86
+ # With parameters (Class objects), it adds new supertypes to this class. Instances of this class will then have role methods for any new superclasses (transitively). Superclasses must be Ruby classes which are existing ObjectTypes.
87
+ # Without parameters, it returns the array of ObjectType supertypes (one by Ruby inheritance, any others as defined using this method)
88
+ def supertypes(*object_types)
89
+ class_eval do
90
+ @supertypes ||= []
91
+ all_supertypes = supertypes_transitive
92
+ object_types.each do |object_type|
93
+ next if all_supertypes.include? object_type
94
+ case object_type
95
+ when Class
96
+ @supertypes << object_type
97
+ when Symbol
98
+ # No late binding here:
99
+ @supertypes << (object_type = vocabulary.const_get(object_type.to_s.camelcase))
100
+ else
101
+ raise "Illegal supertype #{object_type.inspect} for #{self.class.basename}"
102
+ end
103
+
104
+ # Realise the roles (create accessors) of this supertype.
105
+ # REVISIT: The existing accessors at the other end will need to allow this class as role counterpart
106
+ # REVISIT: Need to check all superclass roles recursively, unless we hit a common supertype
107
+ #puts "Realising object_type #{object_type.name} in #{basename}"
108
+ realise_supertypes(object_type, all_supertypes)
109
+ end
110
+ [(superclass.vocabulary && superclass rescue nil), *@supertypes].compact
111
+ end
112
+ end
113
+
114
+ # Return the array of all ObjectType supertypes, transitively.
115
+ def supertypes_transitive
116
+ class_eval do
117
+ supertypes = []
118
+ supertypes << superclass if Module === (superclass.vocabulary rescue nil)
119
+ supertypes += (@supertypes ||= [])
120
+ supertypes.inject([]) {|a, t|
121
+ next if a.include?(t)
122
+ a += [t]
123
+ a += t.supertypes_transitive rescue []
124
+ }.uniq
125
+ end
126
+ end
127
+
128
+ def subtypes
129
+ @subtypes ||= []
130
+ end
131
+
132
+ # Every new role added or inherited comes through here:
133
+ def realise_role(role) #:nodoc:
134
+ #puts "Realising role #{role.counterpart_object_type.basename rescue role.counterpart_object_type}.#{role.name} in #{basename}"
135
+
136
+ if (!role.counterpart)
137
+ # Unary role
138
+ define_unary_role_accessor(role)
139
+ elsif (role.unique)
140
+ define_single_role_accessor(role, role.counterpart.unique)
141
+ else
142
+ define_array_role_accessor(role)
143
+ end
144
+ end
145
+
146
+ # REVISIT: Use method_missing to catch all_some_role_as_other_role_and_third_role, to sort_by those roles?
147
+
148
+ def is_a? klass
149
+ super || supertypes_transitive.include?(klass)
150
+ end
151
+
152
+ private
153
+
154
+ def realise_supertypes(object_type, all_supertypes = nil)
155
+ all_supertypes ||= supertypes_transitive
156
+ s = object_type.supertypes
157
+ #puts "realising #{object_type.basename} supertypes #{s.inspect} of #{basename}"
158
+ s.each {|t|
159
+ next if all_supertypes.include? t
160
+ realise_supertypes(t, all_supertypes)
161
+ t.subtypes << self
162
+ all_supertypes << t
163
+ }
164
+ #puts "Realising roles of #{object_type.basename} in #{basename}"
165
+ realise_roles(object_type)
166
+ end
167
+
168
+ # Realise all the roles of a object_type on this object_type, used when a supertype is added:
169
+ def realise_roles(object_type)
170
+ object_type.roles.each do |role_name, role|
171
+ realise_role(role)
172
+ end
173
+ end
174
+
175
+ # Shared code for both kinds of binary fact type (has_one and one_to_one)
176
+ def define_binary_fact_type(one_to_one, role_name, related, mandatory, related_role_name)
177
+ # puts "#{self}.#{role_name} is to #{related.inspect}, #{mandatory ? :mandatory : :optional}, related role is #{related_role_name}"
178
+
179
+ raise "#{name} cannot have more than one role named #{role_name}" if roles[role_name]
180
+ roles[role_name] = role = Role.new(self, related, nil, role_name, mandatory)
181
+
182
+ # There may be a forward reference here where role_name is a Symbol,
183
+ # and the block runs later when that Symbol is bound to the object_type.
184
+ when_bound(related, self, role_name, related_role_name) do |target, definer, role_name, related_role_name|
185
+ if (one_to_one)
186
+ target.roles[related_role_name] = role.counterpart = Role.new(target, definer, role, related_role_name, false)
187
+ else
188
+ target.roles[related_role_name] = role.counterpart = Role.new(target, definer, role, related_role_name, false, false)
189
+ end
190
+ role.counterpart_object_type = target
191
+ #puts "Realising role pair #{definer.basename}.#{role_name} <-> #{target.basename}.#{related_role_name}"
192
+ realise_role(role)
193
+ target.realise_role(role.counterpart)
194
+ end
195
+ end
196
+
197
+ def define_unary_role_accessor(role)
198
+ # puts "Defining #{basename}.#{role_name} as unary"
199
+ class_eval do
200
+ define_method "#{role.name}=" do |value|
201
+ #puts "Setting #{self.class.name} #{object_id}.@#{role.name} to #{(value ? true : nil).inspect}"
202
+ instance_variable_set("@#{role.name}", value ? true : nil)
203
+ # REVISIT: Provide a way to find all instances playing/not playing this role
204
+ # Analogous to true.all_thing_as_role_name...
205
+ end
206
+ end
207
+ define_single_role_getter(role)
208
+ end
209
+
210
+ def define_single_role_getter(role)
211
+ class_eval do
212
+ define_method role.name do |*a|
213
+ raise "Parameters passed to #{self.class.name}\##{role.name}" if a.size > 0
214
+ i = instance_variable_get("@#{role.name}") rescue nil
215
+ i ? RoleProxy.new(role, i) : i
216
+ i
217
+ end
218
+ end
219
+ end
220
+
221
+ # REVISIT: Add __add_to(constellation) and __remove(constellation) here?
222
+ def define_single_role_accessor(role, one_to_one)
223
+ # puts "Defining #{basename}.#{role.name} to #{role.counterpart_object_type.basename} (#{one_to_one ? "assigning" : "populating"} #{role.counterpart.name})"
224
+ define_single_role_getter(role)
225
+
226
+ if (one_to_one)
227
+ # This gets called to assign nil to the related role in the old correspondent:
228
+ # value is included here so we can check that the correct value is being nullified, if necessary
229
+ nullify_reference = lambda{|from, role_name, value| from.send("#{role_name}=".to_sym, nil) }
230
+
231
+ # This gets called to replace an old single value for a new one in the related role of a new correspondent
232
+ assign_reference = lambda{|from, role_name, old_value, value| from.send("#{role_name}=".to_sym, value) }
233
+
234
+ define_single_role_setter(role, nullify_reference, assign_reference)
235
+ else
236
+ # This gets called to delete this object from the role value array in the old correspondent
237
+ delete_reference = lambda{|from, role_name, value| from.send(role_name).update(value, nil) }
238
+
239
+ # This gets called to replace an old value by a new one in the related role value array of a new correspondent
240
+ replace_reference = lambda{|from, role_name, old_value, value|
241
+ from.send(role_name).update(old_value, value)
242
+ }
243
+
244
+ define_single_role_setter(role, delete_reference, replace_reference)
245
+ end
246
+ end
247
+
248
+ def define_single_role_setter(role, deassign_old, assign_new)
249
+ class_eval do
250
+ define_method "#{role.name}=" do |value|
251
+ role_var = "@#{role.name}"
252
+
253
+ # If role.counterpart_object_type isn't bound to a class yet, bind it.
254
+ role.resolve_counterpart(self.class.vocabulary) unless role.counterpart_object_type.is_a?(Class)
255
+
256
+ # Get old value, and jump out early if it's unchanged:
257
+ old = instance_variable_get(role_var) rescue nil
258
+ return if old == value # Occurs during one_to_one assignment, for example
259
+
260
+ value = role.adapt(constellation, value) if value
261
+ return if old == value # Occurs when same value is assigned
262
+
263
+ # DEBUG: puts "assign #{self.class.basename}.#{role.name} <-> #{value.inspect}.#{role.counterpart.name}#{old ? " (was #{old.inspect})" : ""}"
264
+
265
+ # REVISIT: A frozen-key solution could be used to allow changing identifying roles.
266
+ # The key would be frozen, allowing indices and counterparts to de-assign,
267
+ # but delay re-assignment until defrosted.
268
+ # That would also allow caching the identifying_role_values, a performance win.
269
+
270
+ # This allows setting and clearing identifying roles, but not changing them.
271
+ raise "#{self.class.basename}: illegal attempt to modify identifying role #{role.name}" if role.is_identifying && value != nil && old != nil
272
+
273
+ # puts "Setting binary #{role_var} to #{value.verbalise}"
274
+ instance_variable_set(role_var, value)
275
+
276
+ # De-assign/remove "self" at the old other end too:
277
+ deassign_old.call(old, role.counterpart.name, self) if old
278
+
279
+ # Assign/add "self" at the other end too:
280
+ assign_new.call(value, role.counterpart.name, old, self) if value
281
+ end
282
+ end
283
+ end
284
+
285
+ def define_array_role_accessor(role)
286
+ class_eval do
287
+ define_method "#{role.name}" do
288
+ unless (r = instance_variable_get(role_var = "@#{role.name}") rescue nil)
289
+ r = instance_variable_set(role_var, RoleValues.new)
290
+ end
291
+ # puts "fetching #{self.class.basename}.#{role.name} array, got #{r.class}, first is #{r[0] ? r[0].verbalise : "nil"}"
292
+ r
293
+ end
294
+ end
295
+ end
296
+
297
+ # Extract the parameters to a role definition and massage them into the right shape.
298
+ #
299
+ # The first parameter, role_name, is mandatory. It may be a Symbol, a String or a Class.
300
+ # New proposed input options:
301
+ # :class => the related class (Class object or Symbol). Not allowed if role_name was a class.
302
+ # :mandatory => true. There must be a related object for this object to be valid.
303
+ # :counterpart => Symbol/String. The name of the counterpart role. Will be to_s.snakecase'd and maybe augmented with "all_" and/or "_as_<role_name>"
304
+ # :reading => "forward/reverse". Forward and reverse readings. Must include MARKERS for the player names. May include adjectives. REVISIT: define MARKERS!
305
+ # LATER:
306
+ # :order => :local_role OR lambda{} (for sort_by)
307
+ # :restrict => Range or Array of Range/value or respond_to?(include?)
308
+ #
309
+ # This function returns an array:
310
+ # [ role_name,
311
+ # related,
312
+ # mandatory,
313
+ # related_role_name ]
314
+ #
315
+ # Role naming rule:
316
+ # "all_" if there may be more than one (only ever on related end)
317
+ # Role Name:
318
+ # If a role name is defined at this end:
319
+ # Role Name
320
+ # else:
321
+ # Leading Adjective
322
+ # Role counterpart_object_type name (not role name)
323
+ # Trailing Adjective
324
+ # "_as_<other_role_name>" if other_role_name != this role counterpart_object_type's name, and not other_player_this_player
325
+ def extract_binary_params(one_to_one, role_name, options)
326
+ # Options:
327
+ # other counterpart_object_type (Symbol or Class)
328
+ # mandatory (:mandatory)
329
+ # other end role name if any (Symbol),
330
+ related = nil
331
+ mandatory = false
332
+ related_role_name = nil
333
+ role_player = self.basename.snakecase
334
+
335
+ role_name = a.name.snakecase.to_sym if Class === role_name
336
+ role_name = role_name.to_sym
337
+
338
+ # The related class might be forward-referenced, so handle a Symbol/String instead of a Class.
339
+ related_name = options.delete(:class)
340
+ case related_name
341
+ when nil
342
+ related = role_name # No :class provided, assume it matches the role_name
343
+ related_name ||= role_name.to_s
344
+ when Class
345
+ related = related_name
346
+ related_name = related_name.basename.to_s.snakecase
347
+ when Symbol, String
348
+ related = related_name
349
+ related_name = related_name.to_s.snakecase
350
+ else
351
+ raise "Invalid type for :class option on :#{role_name}"
352
+ end
353
+
354
+ # resolve the Symbol to a Class now if possible:
355
+ resolved = vocabulary.object_type(related) rescue nil
356
+ #puts "#{related} resolves to #{resolved}"
357
+ related = resolved if resolved
358
+ # puts "related = #{related.inspect}"
359
+
360
+ if options.delete(:mandatory) == true
361
+ mandatory = true
362
+ end
363
+
364
+ related_role_name = related_role_name.to_s if related_role_name = options.delete(:counterpart)
365
+
366
+ reading = options.delete(:reading) # REVISIT: Implement verbalisation
367
+ role_value_constraint = options.delete(:restrict) # REVISIT: Implement role value constraints
368
+
369
+ raise "Unrecognised options on #{role_name}: #{options.keys.inspect}" unless options.empty?
370
+
371
+ # Avoid a confusing mismatch:
372
+ # Note that if you have a role "supervisor" and a sub-class "Supervisor", this'll bitch.
373
+ if (Class === related && (indicated = vocabulary.object_type(role_name)) && indicated != related)
374
+ raise "Role name #{role_name} indicates a different counterpart object_type #{indicated} than specified"
375
+ end
376
+
377
+ # This code probably isn't as quick or simple as it could be, but it does work right,
378
+ # and that was pretty hard, because the variable naming is all over the shop. Should fix
379
+ # the naming first (here and in generate/oo.rb) then figure out how to speed it up.
380
+ # Note that oo.rb names things from the opposite end, so you wind up in a maze of mirrors.
381
+ other_role_method =
382
+ (one_to_one ? "" : "all_") +
383
+ (related_role_name || role_player)
384
+ if role_name.to_s != related_name and
385
+ (!related_role_name || related_role_name == role_player)
386
+ other_role_method += "_as_#{role_name}"
387
+ end
388
+ #puts "On #{basename}: have related_role_name=#{related_role_name.inspect}, role_player=#{role_player}, role_name=#{role_name}, related_name=#{related_name.inspect} -> #{related_name}.#{other_role_method}"
389
+
390
+ [ role_name,
391
+ related,
392
+ mandatory,
393
+ other_role_method.to_sym
394
+ ]
395
+ end
396
+
397
+ def when_bound(object_type, *args, &block)
398
+ case object_type
399
+ when Class
400
+ block.call(object_type, *args) # Execute block in the context of the object_type
401
+ when Symbol
402
+ vocabulary.__delay(object_type.to_s.camelcase, args, &block)
403
+ when String # Arrange for this to happen later
404
+ vocabulary.__delay(object_type, args, &block)
405
+ else
406
+ raise "Delayed binding not possible for #{object_type.class.name} #{object_type.inspect}"
407
+ end
408
+ end
409
+ end
410
+ end
411
+ end