activefacts 0.7.0 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +0 -3
- data/Rakefile +7 -5
- data/bin/afgen +5 -2
- data/bin/cql +3 -2
- data/examples/CQL/Genealogy.cql +3 -3
- data/examples/CQL/Metamodel.cql +10 -7
- data/examples/CQL/MultiInheritance.cql +2 -1
- data/examples/CQL/OilSupply.cql +4 -4
- data/examples/CQL/Orienteering.cql +2 -2
- data/lib/activefacts.rb +2 -1
- data/lib/activefacts/api.rb +21 -2
- data/lib/activefacts/api/concept.rb +52 -39
- data/lib/activefacts/api/constellation.rb +8 -6
- data/lib/activefacts/api/entity.rb +41 -37
- data/lib/activefacts/api/instance.rb +5 -3
- data/lib/activefacts/api/numeric.rb +28 -21
- data/lib/activefacts/api/role.rb +29 -43
- data/lib/activefacts/api/standard_types.rb +8 -3
- data/lib/activefacts/api/support.rb +4 -4
- data/lib/activefacts/api/value.rb +9 -3
- data/lib/activefacts/api/vocabulary.rb +17 -7
- data/lib/activefacts/cql.rb +10 -7
- data/lib/activefacts/cql/CQLParser.treetop +6 -0
- data/lib/activefacts/cql/Concepts.treetop +32 -26
- data/lib/activefacts/cql/DataTypes.treetop +6 -0
- data/lib/activefacts/cql/Expressions.treetop +6 -0
- data/lib/activefacts/cql/FactTypes.treetop +6 -0
- data/lib/activefacts/cql/Language/English.treetop +9 -3
- data/lib/activefacts/cql/LexicalRules.treetop +6 -0
- data/lib/activefacts/cql/Rakefile +8 -0
- data/lib/activefacts/cql/parser.rb +4 -2
- data/lib/activefacts/generate/absorption.rb +20 -28
- data/lib/activefacts/generate/cql.rb +28 -16
- data/lib/activefacts/generate/cql/html.rb +327 -321
- data/lib/activefacts/generate/null.rb +7 -3
- data/lib/activefacts/generate/oo.rb +19 -15
- data/lib/activefacts/generate/ordered.rb +457 -461
- data/lib/activefacts/generate/ruby.rb +12 -4
- data/lib/activefacts/generate/sql/server.rb +42 -10
- data/lib/activefacts/generate/text.rb +7 -3
- data/lib/activefacts/input/cql.rb +55 -28
- data/lib/activefacts/input/orm.rb +32 -22
- data/lib/activefacts/persistence.rb +5 -0
- data/lib/activefacts/persistence/columns.rb +66 -32
- data/lib/activefacts/persistence/foreignkey.rb +29 -5
- data/lib/activefacts/persistence/index.rb +57 -25
- data/lib/activefacts/persistence/reference.rb +65 -30
- data/lib/activefacts/persistence/tables.rb +28 -17
- data/lib/activefacts/support.rb +8 -0
- data/lib/activefacts/version.rb +7 -1
- data/lib/activefacts/vocabulary.rb +4 -2
- data/lib/activefacts/vocabulary/extensions.rb +12 -10
- data/lib/activefacts/vocabulary/metamodel.rb +24 -23
- data/spec/api/autocounter.rb +2 -2
- data/spec/api/entity_type.rb +2 -2
- data/spec/api/instance.rb +61 -30
- data/spec/api/roles.rb +9 -9
- data/spec/cql_parse_spec.rb +1 -0
- data/spec/norma_tables_spec.rb +3 -3
- metadata +8 -4
@@ -1,10 +1,12 @@
|
|
1
1
|
#
|
2
|
-
#
|
3
|
-
#
|
2
|
+
# ActiveFacts Runtime API
|
3
|
+
# Constellation class
|
4
|
+
#
|
5
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
4
6
|
#
|
5
7
|
|
6
8
|
module ActiveFacts
|
7
|
-
module API
|
9
|
+
module API #:nodoc:
|
8
10
|
# A Constellation is a population of instances of the Concept classes of a Vocabulary.
|
9
11
|
# Every concept class is either a Value type or an Entity type.
|
10
12
|
#
|
@@ -37,7 +39,7 @@ module ActiveFacts
|
|
37
39
|
# Create a new empty Constellation over the given Vocabulary
|
38
40
|
def initialize(vocabulary)
|
39
41
|
@vocabulary = vocabulary
|
40
|
-
@instances = Hash.new{|h,k| h[k] =
|
42
|
+
@instances = Hash.new{|h,k| h[k] = InstanceIndex.new }
|
41
43
|
end
|
42
44
|
|
43
45
|
def inspect #:nodoc:
|
@@ -52,7 +54,7 @@ module ActiveFacts
|
|
52
54
|
end
|
53
55
|
end
|
54
56
|
|
55
|
-
# With parameters, assert an instance of the concept whose name is the missing method
|
57
|
+
# With parameters, assert an instance of the concept whose name is the missing method, identified by the values passed as *args*.
|
56
58
|
# With no parameters, return the collection of all instances of that concept.
|
57
59
|
def method_missing(m, *args)
|
58
60
|
if klass = @vocabulary.const_get(m)
|
@@ -77,7 +79,7 @@ module ActiveFacts
|
|
77
79
|
|
78
80
|
# REVISIT: It would be better not to rely on the role name pattern here:
|
79
81
|
single_roles, multiple_roles = klass.roles.keys.sort_by(&:to_s).partition{|r| r.to_s !~ /\Aall_/ }
|
80
|
-
single_roles -= klass.
|
82
|
+
single_roles -= klass.identifying_role_names if (klass.respond_to?(:identifying_role_names))
|
81
83
|
# REVISIT: Need to include superclass roles also.
|
82
84
|
|
83
85
|
instances = send(concept.to_sym)
|
@@ -1,12 +1,13 @@
|
|
1
1
|
#
|
2
|
-
#
|
3
|
-
#
|
2
|
+
# ActiveFacts Runtime API
|
3
|
+
# Entity class (a mixin module for the class Class)
|
4
4
|
#
|
5
|
-
#
|
6
|
-
# All Entity types must have an identifier made up of one or more roles.
|
5
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
7
6
|
#
|
8
7
|
module ActiveFacts
|
9
8
|
module API
|
9
|
+
# An Entity type is any Concept that isn't a value type.
|
10
|
+
# All Entity types must have an identifier made up of one or more roles.
|
10
11
|
module Entity
|
11
12
|
include Instance
|
12
13
|
|
@@ -25,23 +26,23 @@ module ActiveFacts
|
|
25
26
|
hash = {}
|
26
27
|
hash = args.pop.clone if Hash === args[-1]
|
27
28
|
|
28
|
-
# Pick any missing
|
29
|
-
while args.size < (ir = klass.
|
29
|
+
# Pick any missing identifying roles out of the hash if possible:
|
30
|
+
while args.size < (ir = klass.identifying_role_names).size
|
30
31
|
value = hash[role = ir[args.size]]
|
31
32
|
hash.delete(role)
|
32
33
|
args.push value
|
33
34
|
end
|
34
35
|
|
35
36
|
# If one arg is expected but more are passed, they might be the args for the object that plays the identifying role:
|
36
|
-
args = [args] if klass.
|
37
|
+
args = [args] if klass.identifying_role_names.size == 1 && args.size > 1
|
37
38
|
|
38
39
|
# This should now only occur when there are too many args passed:
|
39
40
|
raise "Wrong number of parameters to #{klass}.new, " +
|
40
|
-
"expect (#{klass.
|
41
|
-
"got (#{args.map{|a| a.to_s.inspect}*", "})" if args.size != klass.
|
41
|
+
"expect (#{klass.identifying_role_names*","}) " +
|
42
|
+
"got (#{args.map{|a| a.to_s.inspect}*", "})" if args.size != klass.identifying_role_names.size
|
42
43
|
|
43
44
|
# Assign the identifying roles in order, then the other roles passed as a hash:
|
44
|
-
(klass.
|
45
|
+
(klass.identifying_role_names.zip(args) + hash.entries).each do |role_name, value|
|
45
46
|
role = klass.roles(role_name)
|
46
47
|
send("#{role_name}=", value)
|
47
48
|
end
|
@@ -56,15 +57,15 @@ module ActiveFacts
|
|
56
57
|
constellation ? " in #{constellation.inspect}" : ""
|
57
58
|
} #{
|
58
59
|
# REVISIT: Where there are one-to-one roles, this cycles
|
59
|
-
self.class.
|
60
|
+
self.class.identifying_role_names.map{|role| "@#{role}="+send(role).inspect }*" "
|
60
61
|
}>"
|
61
62
|
end
|
62
63
|
|
63
64
|
# When used as a hash key, the hash key of this entity instance is calculated
|
64
65
|
# by hashing the values of its identifying roles
|
65
66
|
def hash
|
66
|
-
self.class.
|
67
|
-
|
67
|
+
self.class.identifying_role_names.map{|role|
|
68
|
+
instance_variable_get("@#{role}")
|
68
69
|
}.inject(0) { |h,v|
|
69
70
|
h ^= v.hash
|
70
71
|
h
|
@@ -75,7 +76,7 @@ module ActiveFacts
|
|
75
76
|
# comparing the values of its identifying roles
|
76
77
|
def eql?(other)
|
77
78
|
return false unless self.class == other.class
|
78
|
-
self.class.
|
79
|
+
self.class.identifying_role_names.each{|role|
|
79
80
|
return false unless send(role).eql?(other.send(role))
|
80
81
|
}
|
81
82
|
return true
|
@@ -84,7 +85,7 @@ module ActiveFacts
|
|
84
85
|
# Verbalise this entity instance
|
85
86
|
def verbalise(role_name = nil)
|
86
87
|
"#{role_name || self.class.basename}(#{
|
87
|
-
self.class.
|
88
|
+
self.class.identifying_role_names.map{|role_sym|
|
88
89
|
value = send(role_sym)
|
89
90
|
role_name = self.class.roles(role_sym).name.to_s.camelcase(true)
|
90
91
|
value ? value.verbalise(role_name) : "nil"
|
@@ -94,7 +95,7 @@ module ActiveFacts
|
|
94
95
|
|
95
96
|
# Return the array of the values of this entity instance's identifying roles
|
96
97
|
def identifying_role_values
|
97
|
-
self.class.
|
98
|
+
self.class.identifying_role_names.map{|role|
|
98
99
|
send(role)
|
99
100
|
}
|
100
101
|
end
|
@@ -104,22 +105,23 @@ module ActiveFacts
|
|
104
105
|
include Instance::ClassMethods
|
105
106
|
|
106
107
|
# Return the array of Role objects that define the identifying relationships of this Entity type:
|
107
|
-
def
|
108
|
-
@
|
108
|
+
def identifying_role_names
|
109
|
+
@identifying_role_names ||= []
|
109
110
|
end
|
110
111
|
|
111
|
-
#
|
112
|
+
# Convert the passed arguments into an array of Instance objects that can identify an instance of this Entity type:
|
112
113
|
def identifying_role_values(*args)
|
113
|
-
#puts "Getting identifying role values #{
|
114
|
+
#puts "Getting identifying role values #{identifying_role_names.inspect} of #{basename} using #{args.inspect}"
|
114
115
|
|
115
116
|
# If the single arg is an instance of the correct class or a subclass,
|
116
117
|
# use the instance's identifying_role_values
|
117
118
|
if (args.size == 1 and
|
118
|
-
|
119
|
-
|
119
|
+
(arg = args[0]).is_a?(self)) # REVISIT: or a secondary supertype
|
120
|
+
arg = arg.__getobj__ if RoleProxy === arg
|
121
|
+
return arg.identifying_role_values
|
120
122
|
end
|
121
123
|
|
122
|
-
ir =
|
124
|
+
ir = identifying_role_names
|
123
125
|
args, arg_hash = ActiveFacts::extract_hash_args(ir, args)
|
124
126
|
if args.size < ir.size
|
125
127
|
raise "#{basename} requires all identifying values, you're missing #{ir[args.size..-1].map(&:to_sym)*', '}"
|
@@ -129,14 +131,15 @@ module ActiveFacts
|
|
129
131
|
|
130
132
|
role_args = ir.map{|role_sym| roles(role_sym)}.zip(args)
|
131
133
|
role_args.map do |role, arg|
|
132
|
-
#puts "Getting identifying_role_value for #{role.
|
134
|
+
#puts "Getting identifying_role_value for #{role.counterpart_concept.basename} using #{arg.inspect}"
|
133
135
|
next nil unless arg
|
134
136
|
next !!arg unless role.counterpart # Unary
|
135
|
-
|
136
|
-
#
|
137
|
+
arg = arg.__getobj__ if RoleProxy === arg
|
138
|
+
if arg.is_a?(role.counterpart_concept) # REVISIT: or a secondary supertype
|
139
|
+
# Note that with a secondary supertype, it must still return the values of these identifying_role_names
|
137
140
|
next arg.identifying_role_values
|
138
141
|
end
|
139
|
-
role.
|
142
|
+
role.counterpart_concept.identifying_role_values(*arg)
|
140
143
|
end
|
141
144
|
end
|
142
145
|
|
@@ -153,7 +156,7 @@ module ActiveFacts
|
|
153
156
|
return instance, key if instance # A matching instance of this class
|
154
157
|
|
155
158
|
# Now construct each of this object's identifying roles
|
156
|
-
ir =
|
159
|
+
ir = identifying_role_names
|
157
160
|
args, arg_hash = ActiveFacts::extract_hash_args(ir, args)
|
158
161
|
role_values = ir.map{|role_sym| roles(role_sym)}.zip(args)
|
159
162
|
key = [] # Gather the actual key (AutoCounters are special)
|
@@ -162,11 +165,12 @@ module ActiveFacts
|
|
162
165
|
value = role_key = nil # No value
|
163
166
|
elsif !role.counterpart
|
164
167
|
value = role_key = !!arg # Unary
|
165
|
-
elsif role.
|
168
|
+
elsif arg.is_a?(role.counterpart_concept) # REVISIT: or a secondary supertype
|
169
|
+
arg = arg.__getobj__ if RoleProxy === arg
|
166
170
|
raise "Connecting values across constellations" unless arg.constellation == constellation
|
167
171
|
value, role_key = arg, arg.identifying_role_values
|
168
172
|
else
|
169
|
-
value, role_key = role.
|
173
|
+
value, role_key = role.counterpart_concept.assert_instance(constellation, Array(arg))
|
170
174
|
end
|
171
175
|
key << role_key
|
172
176
|
value
|
@@ -183,8 +187,8 @@ module ActiveFacts
|
|
183
187
|
|
184
188
|
def index_instance(instance, key = nil, key_roles = nil) #:nodoc:
|
185
189
|
# Derive a new key if we didn't receive one or if the roles are different:
|
186
|
-
unless key && key_roles && key_roles ==
|
187
|
-
key = (key_roles =
|
190
|
+
unless key && key_roles && key_roles == identifying_role_names
|
191
|
+
key = (key_roles = identifying_role_names).map do |role_name|
|
188
192
|
instance.send role_name
|
189
193
|
end
|
190
194
|
end
|
@@ -207,20 +211,20 @@ module ActiveFacts
|
|
207
211
|
# inherited from a superclass.
|
208
212
|
def initialise_entity_type(*args) #:nodoc:
|
209
213
|
#puts "Initialising entity type #{self} using #{args.inspect}"
|
210
|
-
@
|
211
|
-
# REVISIT: @
|
214
|
+
@identifying_role_names = superclass.identifying_role_names if superclass.respond_to?(:identifying_role_names)
|
215
|
+
# REVISIT: @identifying_role_names here are the symbols passed in, not the Role objects we should use.
|
212
216
|
# We'd need late binding to use Role objects...
|
213
|
-
@
|
217
|
+
@identifying_role_names = args if args.size > 0 || !@identifying_role_names
|
214
218
|
end
|
215
219
|
|
216
220
|
def inherited(other) #:nodoc:
|
217
|
-
other.identified_by *
|
221
|
+
other.identified_by *identifying_role_names
|
218
222
|
vocabulary.add_concept(other)
|
219
223
|
end
|
220
224
|
|
221
225
|
# verbalise this concept
|
222
226
|
def verbalise
|
223
|
-
"#{basename}
|
227
|
+
"#{basename} is identified by #{identifying_role_names.map{|role_sym| role_sym.to_s.camelcase(true)}*" and "};"
|
224
228
|
end
|
225
229
|
end
|
226
230
|
|
@@ -1,6 +1,8 @@
|
|
1
1
|
#
|
2
|
-
#
|
3
|
-
#
|
2
|
+
# ActiveFacts Runtime API
|
3
|
+
# Instance (mixin module for instances of a Concept - a class with Concept mixed in)
|
4
|
+
#
|
5
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
4
6
|
#
|
5
7
|
# Instance methods are extended into all instances, whether of value or entity types.
|
6
8
|
#
|
@@ -12,7 +14,7 @@ module ActiveFacts
|
|
12
14
|
attr_accessor :constellation
|
13
15
|
|
14
16
|
def initialize(args = []) #:nodoc:
|
15
|
-
unless (self.class.respond_to?(:
|
17
|
+
unless (self.class.respond_to?(:identifying_role_names))
|
16
18
|
#if (self.class.superclass != Object)
|
17
19
|
# puts "constructing #{self.class.superclass} with #{args.inspect}"
|
18
20
|
super(*args)
|
@@ -1,32 +1,35 @@
|
|
1
1
|
#
|
2
|
-
#
|
3
|
-
#
|
2
|
+
# ActiveFacts Runtime API
|
3
|
+
# Numeric and Date delegates and hacks to handle immediate types.
|
4
4
|
#
|
5
|
-
#
|
5
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
6
|
+
#
|
7
|
+
# These delegates are required because Integer & Float don't support new,
|
6
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.
|
7
10
|
#
|
8
11
|
require 'delegate'
|
9
12
|
require 'date'
|
10
13
|
|
11
14
|
# It's not possible to subclass Integer, so instead we delegate to it.
|
12
15
|
class Int < SimpleDelegator
|
13
|
-
def initialize(i = nil)
|
16
|
+
def initialize(i = nil) #:nodoc:
|
14
17
|
__setobj__(Integer(i))
|
15
18
|
end
|
16
19
|
|
17
|
-
def to_s
|
20
|
+
def to_s #:nodoc:
|
18
21
|
__getobj__.to_s
|
19
22
|
end
|
20
23
|
|
21
|
-
def hash
|
24
|
+
def hash #:nodoc:
|
22
25
|
__getobj__.hash ^ self.class.hash
|
23
26
|
end
|
24
27
|
|
25
|
-
def eql?(o)
|
28
|
+
def eql?(o) #:nodoc:
|
26
29
|
self.class == o.class and __getobj__.eql?(Integer(o))
|
27
30
|
end
|
28
31
|
|
29
|
-
def ==(o)
|
32
|
+
def ==(o) #:nodoc:
|
30
33
|
__getobj__.==(o)
|
31
34
|
end
|
32
35
|
|
@@ -37,34 +40,35 @@ end
|
|
37
40
|
|
38
41
|
# It's not possible to subclass Float, so instead we delegate to it.
|
39
42
|
class Real < SimpleDelegator
|
40
|
-
def initialize(r = nil)
|
43
|
+
def initialize(r = nil) #:nodoc:
|
41
44
|
__setobj__(Float(r))
|
42
45
|
end
|
43
46
|
|
44
|
-
def hash
|
47
|
+
def hash #:nodoc:
|
45
48
|
__getobj__.hash ^ self.class.hash
|
46
49
|
end
|
47
50
|
|
48
|
-
def to_s
|
51
|
+
def to_s #:nodoc:
|
49
52
|
__getobj__.to_s
|
50
53
|
end
|
51
54
|
|
52
|
-
def eql?(o)
|
55
|
+
def eql?(o) #:nodoc:
|
53
56
|
self.class == o.class and __getobj__.eql?(Float(o))
|
54
57
|
end
|
55
58
|
|
56
|
-
def ==(o)
|
59
|
+
def ==(o) #:nodoc:
|
57
60
|
__getobj__.==(o)
|
58
61
|
end
|
59
62
|
|
60
|
-
def inspect
|
63
|
+
def inspect #:nodoc:
|
61
64
|
"#{self.class.basename}:#{__getobj__.inspect}"
|
62
65
|
end
|
63
66
|
end
|
64
67
|
|
65
68
|
# A Date can be constructed from any Date subclass, not just using the normal date constructors.
|
66
|
-
class ::Date
|
69
|
+
class ::Date
|
67
70
|
class << self; alias_method :old_new, :new end
|
71
|
+
# Date.new cannot normally be called passing a Date as the parameter. This allows that.
|
68
72
|
def self.new(*a, &b)
|
69
73
|
#puts "Constructing date with #{a.inspect} from #{caller*"\n\t"}"
|
70
74
|
if (a.size == 1 && Date === a[0])
|
@@ -77,8 +81,9 @@ class ::Date #:nodoc:
|
|
77
81
|
end
|
78
82
|
|
79
83
|
# A DateTime can be constructed from any Date or DateTime subclass
|
80
|
-
class ::DateTime
|
84
|
+
class ::DateTime
|
81
85
|
class << self; alias_method :old_new, :new end
|
86
|
+
# DateTime.new cannot normally be called passing a Date or DateTime as the parameter. This allows that.
|
82
87
|
def self.new(*a, &b)
|
83
88
|
#puts "Constructing DateTime with #{a.inspect} from #{caller*"\n\t"}"
|
84
89
|
if (a.size == 1)
|
@@ -99,8 +104,7 @@ end
|
|
99
104
|
# The AutoCounter class is an integer, but only after the value
|
100
105
|
# has been established in the database.
|
101
106
|
# Construct it with the value :new to get an uncommitted value.
|
102
|
-
# You can use this new instance as a role
|
103
|
-
# or anywhere else for that matter.
|
107
|
+
# You can use this new instance as a value of any role of this type, including to identify an entity instance.
|
104
108
|
# The assigned value will be filled out everywhere it needs to be, upon save.
|
105
109
|
class AutoCounter
|
106
110
|
def initialize(i = :new)
|
@@ -109,11 +113,13 @@ class AutoCounter
|
|
109
113
|
@value = i == :new ? nil : i
|
110
114
|
end
|
111
115
|
|
116
|
+
# Assign a definite value to an AutoCounter; this may only be done once
|
112
117
|
def assign(i)
|
113
118
|
raise ArgumentError if @value
|
114
119
|
@value = i.to_i
|
115
120
|
end
|
116
121
|
|
122
|
+
# Ask whether a definite value has been assigned
|
117
123
|
def defined?
|
118
124
|
!@value.nil?
|
119
125
|
end
|
@@ -126,6 +132,7 @@ class AutoCounter
|
|
126
132
|
end
|
127
133
|
end
|
128
134
|
|
135
|
+
# An AutoCounter may only be used in numeric expressions after a definite value has been assigned
|
129
136
|
def self.coerce(i)
|
130
137
|
raise ArgumentError unless @value
|
131
138
|
[ i.to_i, @value ]
|
@@ -135,15 +142,15 @@ class AutoCounter
|
|
135
142
|
"\#<AutoCounter "+to_s+">"
|
136
143
|
end
|
137
144
|
|
138
|
-
def hash
|
145
|
+
def hash #:nodoc:
|
139
146
|
to_s.hash ^ self.class.hash
|
140
147
|
end
|
141
148
|
|
142
|
-
def eql?(o)
|
149
|
+
def eql?(o) #:nodoc:
|
143
150
|
self.class == o.class and to_s.eql?(o.to_s)
|
144
151
|
end
|
145
152
|
|
146
|
-
def self.inherited(other)
|
153
|
+
def self.inherited(other) #:nodoc:
|
147
154
|
def other.identifying_role_values(*args)
|
148
155
|
return nil if args == [:new] # A new object has no identifying_role_values
|
149
156
|
return new(*args)
|
data/lib/activefacts/api/role.rb
CHANGED
@@ -1,5 +1,10 @@
|
|
1
|
-
|
2
|
-
#
|
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.
|
3
8
|
#
|
4
9
|
module ActiveFacts
|
5
10
|
module API
|
@@ -9,37 +14,43 @@ module ActiveFacts
|
|
9
14
|
# or _one_to_one_, and the other is created on the counterpart class. Each Concept class maintains
|
10
15
|
# an array of the roles it plays.
|
11
16
|
class Role
|
12
|
-
attr_accessor :
|
17
|
+
attr_accessor :owner # The Concept to which this role belongs
|
18
|
+
attr_accessor :name # The name of the role (a Symbol)
|
19
|
+
attr_accessor :counterpart_concept # A Concept Class (may be temporarily a Symbol before the class is defined)
|
13
20
|
attr_accessor :counterpart # All roles except unaries have a binary counterpart
|
14
|
-
attr_accessor :
|
15
|
-
attr_accessor :
|
16
|
-
attr_accessor :
|
17
|
-
|
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_restriction # Counterpart Instances playing this role must meet these restrictions
|
24
|
+
attr_reader :is_identifying # Is this an identifying role for owner?
|
18
25
|
|
19
|
-
def initialize(
|
20
|
-
@
|
26
|
+
def initialize(owner, counterpart_concept, counterpart, name, mandatory = false, unique = true)
|
27
|
+
@owner = owner
|
28
|
+
@counterpart_concept = counterpart_concept
|
21
29
|
@counterpart = counterpart
|
22
30
|
@name = name
|
23
31
|
@mandatory = mandatory
|
24
32
|
@unique = unique
|
33
|
+
@is_identifying = @owner.respond_to?(:identifying_role_names) && @owner.identifying_role_names.include?(@name)
|
25
34
|
end
|
26
35
|
|
36
|
+
# Is this role a unary (created by maybe)? If so, it has no counterpart
|
27
37
|
def unary?
|
28
38
|
# N.B. A role with a forward reference looks unary until it is resolved.
|
29
39
|
counterpart == nil
|
30
40
|
end
|
31
41
|
|
32
|
-
def
|
33
|
-
return @
|
34
|
-
klass = vocabulary.concept(@
|
35
|
-
raise "Cannot resolve role
|
36
|
-
@
|
42
|
+
def resolve_counterpart(vocabulary) #:nodoc:
|
43
|
+
return @counterpart_concept if @counterpart_concept.is_a?(Class) # Done already
|
44
|
+
klass = vocabulary.concept(@counterpart_concept) # Trigger the binding
|
45
|
+
raise "Cannot resolve role counterpart_concept #{@counterpart_concept.inspect} for role #{name} in vocabulary #{vocabulary.basename}; still forward-declared?" unless klass
|
46
|
+
@counterpart_concept = klass # Memoize a successful result
|
37
47
|
end
|
38
48
|
|
39
49
|
def adapt(constellation, value) #:nodoc:
|
40
50
|
# If the value is a compatible class, use it (if in another constellation, clone it),
|
41
51
|
# else create a compatible object using the value as constructor parameters.
|
42
|
-
if @
|
52
|
+
if value.is_a?(@counterpart_concept) # REVISIT: may be a non-primary subtype of counterpart_concept
|
53
|
+
value = value.__getobj__ if RoleProxy === value
|
43
54
|
# Check that the value is in a compatible constellation, clone if not:
|
44
55
|
if constellation && (vc = value.constellation) && vc != constellation
|
45
56
|
value = value.clone # REVISIT: There's sure to be things we should reset/clone here, like non-identifying roles
|
@@ -47,11 +58,11 @@ module ActiveFacts
|
|
47
58
|
value.constellation = constellation if constellation
|
48
59
|
else
|
49
60
|
value = [value] unless Array === value
|
50
|
-
raise "No parameters were provided to identify an #{@
|
61
|
+
raise "No parameters were provided to identify an #{@counterpart_concept.basename} instance" if value == []
|
51
62
|
if constellation
|
52
|
-
value = constellation.send(@
|
63
|
+
value = constellation.send(@counterpart_concept.basename.to_sym, *value)
|
53
64
|
else
|
54
|
-
value = @
|
65
|
+
value = @counterpart_concept.new(*value)
|
55
66
|
end
|
56
67
|
end
|
57
68
|
value
|
@@ -65,30 +76,5 @@ module ActiveFacts
|
|
65
76
|
keys.sort_by(&:to_s).inspect
|
66
77
|
end
|
67
78
|
end
|
68
|
-
|
69
|
-
# A RoleValueArray is an array with all mutating methods hidden.
|
70
|
-
# We use these for the "many" side of a 1:many relationship.
|
71
|
-
# Only "replace" and "delete" are actually used (so far!).
|
72
|
-
#
|
73
|
-
# Don't rely on this implementation, as it must change to support
|
74
|
-
# persistence.
|
75
|
-
#
|
76
|
-
class RoleValueArray < Array #:nodoc:
|
77
|
-
[ :"<<", :"[]=", :clear, :collect!, :compact!, :concat, :delete,
|
78
|
-
:delete_at, :delete_if, :fill, :flatten!, :insert, :map!, :pop,
|
79
|
-
:push, :reject!, :replace, :reverse!, :shift, :shuffle!, :slice!,
|
80
|
-
:sort!, :uniq!, :unshift
|
81
|
-
].each{|s|
|
82
|
-
begin
|
83
|
-
alias_method("__#{s}", s)
|
84
|
-
rescue NameError # shuffle! is in 1.9 only
|
85
|
-
end
|
86
|
-
}
|
87
|
-
|
88
|
-
def verbalise
|
89
|
-
"["+map{|e| e.verbalise}*", "+"]"
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
79
|
end
|
94
80
|
end
|