activefacts-api 0.8.9
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/LICENSE.txt +19 -0
- data/README.rdoc +41 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/lib/activefacts/api.rb +44 -0
- data/lib/activefacts/api/constellation.rb +128 -0
- data/lib/activefacts/api/entity.rb +260 -0
- data/lib/activefacts/api/instance.rb +60 -0
- data/lib/activefacts/api/instance_index.rb +84 -0
- data/lib/activefacts/api/numeric.rb +175 -0
- data/lib/activefacts/api/object_type.rb +411 -0
- data/lib/activefacts/api/role.rb +80 -0
- data/lib/activefacts/api/role_proxy.rb +71 -0
- data/lib/activefacts/api/role_values.rb +117 -0
- data/lib/activefacts/api/standard_types.rb +87 -0
- data/lib/activefacts/api/support.rb +66 -0
- data/lib/activefacts/api/value.rb +135 -0
- data/lib/activefacts/api/vocabulary.rb +82 -0
- data/spec/api/autocounter_spec.rb +84 -0
- data/spec/api/constellation_spec.rb +129 -0
- data/spec/api/entity_type_spec.rb +103 -0
- data/spec/api/instance_spec.rb +462 -0
- data/spec/api/roles_spec.rb +124 -0
- data/spec/api/value_type_spec.rb +114 -0
- data/spec/spec_helper.rb +12 -0
- metadata +154 -0
@@ -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
|