activefacts-metamodel 1.7.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/activefacts/metamodel.rb +2 -623
- data/lib/activefacts/metamodel/extensions.rb +1358 -0
- data/lib/activefacts/metamodel/metamodel.rb +623 -0
- data/lib/activefacts/metamodel/version.rb +1 -1
- metadata +3 -1
@@ -0,0 +1,1358 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Vocabulary Metamodel.
|
3
|
+
# Extensions to the ActiveFacts Vocabulary classes (which are generated from the Metamodel)
|
4
|
+
#
|
5
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
6
|
+
#
|
7
|
+
module ActiveFacts
|
8
|
+
module Metamodel
|
9
|
+
class Vocabulary
|
10
|
+
def finalise
|
11
|
+
constellation.FactType.values.each do |fact_type|
|
12
|
+
if c = fact_type.check_and_add_spanning_uniqueness_constraint
|
13
|
+
trace :constraint, "Checking for existence of at least one uniqueness constraint over the roles of #{fact_type.default_reading.inspect}"
|
14
|
+
fact_type.check_and_add_spanning_uniqueness_constraint = nil
|
15
|
+
c.call
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# This name does not yet exist (at least not as we expect it to).
|
21
|
+
# If it in fact does exist (but as the wrong type), complain.
|
22
|
+
# If it doesn't exist, but its name would cause existing fact type
|
23
|
+
# readings to be re-interpreted to a different meaning, complain.
|
24
|
+
# Otherwise return nil.
|
25
|
+
def check_valid_nonexistent_object_type_name name
|
26
|
+
if ot = valid_object_type_name(name)
|
27
|
+
raise "Cannot redefine #{ot.class.basename} #{name}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def valid_object_type_name name
|
32
|
+
# Raise an exception if adding this name to the vocabulary would create anomalies
|
33
|
+
anomaly = constellation.Reading.detect do |r_key, reading|
|
34
|
+
expanded = reading.expand do |role_ref, *words|
|
35
|
+
words.map! do |w|
|
36
|
+
case
|
37
|
+
when w == nil
|
38
|
+
w
|
39
|
+
when w[0...name.size] == name
|
40
|
+
'_ok_'+w
|
41
|
+
when w[-name.size..-1] == name
|
42
|
+
w[-1]+'_ok_'
|
43
|
+
else
|
44
|
+
w
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
words
|
49
|
+
end
|
50
|
+
expanded =~ %r{\b#{name}\b}
|
51
|
+
end
|
52
|
+
raise "Adding new term '#{name}' would create anomalous re-interpretation of '#{anomaly.expand}'" if anomaly
|
53
|
+
@constellation.ObjectType[[identifying_role_values, name]]
|
54
|
+
end
|
55
|
+
|
56
|
+
# If this entity type exists, ok, otherwise check it's ok to add it
|
57
|
+
def valid_entity_type_name name
|
58
|
+
@constellation.EntityType[[identifying_role_values, name]] or
|
59
|
+
check_valid_nonexistent_object_type_name name
|
60
|
+
end
|
61
|
+
|
62
|
+
# If this entity type exists, ok, otherwise check it's ok to add it
|
63
|
+
def valid_value_type_name name
|
64
|
+
@constellation.ValueType[[identifying_role_values, name]] or
|
65
|
+
check_valid_nonexistent_object_type_name name
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Concept
|
70
|
+
def describe
|
71
|
+
case
|
72
|
+
when object_type; "#{object_type.class.basename} #{object_type.name.inspect}"
|
73
|
+
when fact_type; "FactType #{fact_type.default_reading.inspect}"
|
74
|
+
when role; "Role in #{role.fact_type.describe(role)}"
|
75
|
+
when constraint; constraint.describe
|
76
|
+
when instance; "Instance #{instance.verbalise}"
|
77
|
+
when fact; "Fact #{fact.verbalise}"
|
78
|
+
when query; query.describe
|
79
|
+
when context_note; "ContextNote#{context_note.verbalise}"
|
80
|
+
when unit; "Unit #{unit.describe}"
|
81
|
+
when population; "Population: #{population.name}"
|
82
|
+
else
|
83
|
+
raise "ROGUE CONCEPT OF NO TYPE"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def embodied_as
|
88
|
+
case
|
89
|
+
when object_type; object_type
|
90
|
+
when fact_type; fact_type
|
91
|
+
when role; role
|
92
|
+
when constraint; constraint
|
93
|
+
when instance; instance
|
94
|
+
when fact; fact
|
95
|
+
when query; query
|
96
|
+
when context_note; context_note
|
97
|
+
when unit; unit
|
98
|
+
when population; population
|
99
|
+
else
|
100
|
+
raise "ROGUE CONCEPT OF NO TYPE"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Return an array of all Concepts that must be defined before this concept can be defined:
|
105
|
+
def precursors
|
106
|
+
case body = embodied_as
|
107
|
+
when ActiveFacts::Metamodel::ValueType
|
108
|
+
[ body.supertype, body.unit ] +
|
109
|
+
body.all_value_type_parameter.map{|f| f.facet_value_type } +
|
110
|
+
body.all_value_type_parameter_restriction.map{|vr| vr.value.unit}
|
111
|
+
when ActiveFacts::Metamodel::EntityType
|
112
|
+
# You can't define the preferred_identifier fact types until you define the entity type,
|
113
|
+
# but the objects which play the identifying roles must be defined:
|
114
|
+
body.preferred_identifier.role_sequence.all_role_ref.map {|rr| rr.role.object_type } +
|
115
|
+
# You can't define the objectified fact type until you define the entity type:
|
116
|
+
# [ body.fact_type ] # If it's an objectification
|
117
|
+
body.all_type_inheritance_as_subtype.map{|ti| ti.supertype} # If it's a subtype
|
118
|
+
when FactType
|
119
|
+
body.all_role.map(&:object_type)
|
120
|
+
when Role # We don't consider roles as they cannot be separately defined
|
121
|
+
[]
|
122
|
+
when ActiveFacts::Metamodel::PresenceConstraint
|
123
|
+
body.role_sequence.all_role_ref.map do |rr|
|
124
|
+
rr.role.fact_type
|
125
|
+
end
|
126
|
+
when ActiveFacts::Metamodel::ValueConstraint
|
127
|
+
[ body.role ? body.role.fact_type : nil, body.value_type ] +
|
128
|
+
body.all_allowed_range.map do |ar|
|
129
|
+
[ ar.value_range.minimum_bound, ar.value_range.maximum_bound ].compact.map{|b| b.value.unit}
|
130
|
+
end
|
131
|
+
when ActiveFacts::Metamodel::SubsetConstraint
|
132
|
+
body.subset_role_sequence.all_role_ref.map{|rr| rr.role.fact_type } +
|
133
|
+
body.superset_role_sequence.all_role_ref.map{|rr| rr.role.fact_type }
|
134
|
+
when ActiveFacts::Metamodel::SetComparisonConstraint
|
135
|
+
body.all_set_comparison_roles.map{|scr| scr.role_sequence.all_role_ref.map{|rr| rr.role.fact_type } }
|
136
|
+
when ActiveFacts::Metamodel::RingConstraint
|
137
|
+
[ body.role.fact_type, body.other_role.fact_type ]
|
138
|
+
when Instance
|
139
|
+
[ body.population, body.object_type, body.value ? body.value.unit : nil ]
|
140
|
+
when Fact
|
141
|
+
[ body.population, body.fact_type ]
|
142
|
+
when Query
|
143
|
+
body.all_variable.map do |v|
|
144
|
+
[ v.object_type,
|
145
|
+
v.value ? v.value.unit : nil,
|
146
|
+
v.step ? v.step.fact_type : nil
|
147
|
+
] +
|
148
|
+
v.all_play.map{|p| p.role.fact_type }
|
149
|
+
end
|
150
|
+
when ContextNote
|
151
|
+
[]
|
152
|
+
when Unit
|
153
|
+
body.all_derivation_as_derived_unit.map{|d| d.base_unit }
|
154
|
+
when Population
|
155
|
+
[]
|
156
|
+
else
|
157
|
+
raise "ROGUE CONCEPT OF NO TYPE"
|
158
|
+
end.flatten.compact.uniq.map{|c| c.concept }
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
class Topic
|
163
|
+
def precursors
|
164
|
+
# Precursors of a topic are the topics of all precursors of items in this topic
|
165
|
+
all_concept.map{|c| c.precursors }.flatten.uniq.map{|c| c.topic}.uniq-[self]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
class Unit
|
170
|
+
def describe
|
171
|
+
'Unit' +
|
172
|
+
name +
|
173
|
+
(plural_name ? '/'+plural_name : '') +
|
174
|
+
'=' +
|
175
|
+
coefficient.to_s+'*' +
|
176
|
+
all_derivation_as_derived_unit.map do |derivation|
|
177
|
+
derivation.base_unit.name +
|
178
|
+
(derivation.exponent != 1 ? derivation.exponent.to_s : '')
|
179
|
+
end.join('') +
|
180
|
+
(offset ? ' + '+offset.to_s : '')
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
class Coefficient
|
185
|
+
def to_s
|
186
|
+
numerator.to_s +
|
187
|
+
(denominator != 1 ? '/' + denominator.to_s : '')
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
class FactType
|
192
|
+
attr_accessor :check_and_add_spanning_uniqueness_constraint
|
193
|
+
|
194
|
+
def all_reading_by_ordinal
|
195
|
+
all_reading.sort_by{|reading| reading.ordinal}
|
196
|
+
end
|
197
|
+
|
198
|
+
def preferred_reading negated = false
|
199
|
+
pr = all_reading_by_ordinal.detect{|r| !r.is_negative == !negated }
|
200
|
+
raise "No reading for (#{all_role.map{|r| r.object_type.name}*", "})" unless pr || negated
|
201
|
+
pr
|
202
|
+
end
|
203
|
+
|
204
|
+
def describe(highlight = nil)
|
205
|
+
(entity_type ? entity_type.name : "")+
|
206
|
+
'('+all_role.map{|role| role.describe(highlight) }*", "+')'
|
207
|
+
end
|
208
|
+
|
209
|
+
def default_reading(frequency_constraints = [], define_role_names = nil)
|
210
|
+
preferred_reading.expand(frequency_constraints, define_role_names)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Does any role of this fact type participate in a preferred identifier?
|
214
|
+
def is_existential
|
215
|
+
return false if all_role.size > 2
|
216
|
+
all_role.detect do |role|
|
217
|
+
role.all_role_ref.detect do |rr|
|
218
|
+
rr.role_sequence.all_presence_constraint.detect do |pc|
|
219
|
+
pc.is_preferred_identifier
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def internal_presence_constraints
|
226
|
+
all_role.map do |r|
|
227
|
+
r.all_role_ref.map do |rr|
|
228
|
+
!rr.role_sequence.all_role_ref.detect{|rr1| rr1.role.fact_type != self } ?
|
229
|
+
rr.role_sequence.all_presence_constraint.to_a :
|
230
|
+
[]
|
231
|
+
end
|
232
|
+
end.flatten.compact.uniq
|
233
|
+
end
|
234
|
+
|
235
|
+
def implicit_boolean_type vocabulary
|
236
|
+
@constellation.ImplicitBooleanValueType[[vocabulary.identifying_role_values, "_ImplicitBooleanValueType"]] or
|
237
|
+
@constellation.ImplicitBooleanValueType(vocabulary.identifying_role_values, "_ImplicitBooleanValueType", :concept => [:new, :implication_rule => 'unary'])
|
238
|
+
end
|
239
|
+
|
240
|
+
# This entity type has just objectified a fact type. Create the necessary ImplicitFactTypes with phantom roles
|
241
|
+
def create_implicit_fact_type_for_unary
|
242
|
+
role = all_role.single
|
243
|
+
return if role.link_fact_type # Already exists
|
244
|
+
# NORMA doesn't create an implicit fact type here, rather the fact type has an implicit extra role, so looks like a binary
|
245
|
+
# We only do it when the unary fact type is not objectified
|
246
|
+
link_fact_type = @constellation.LinkFactType(:new, :implying_role => role)
|
247
|
+
link_fact_type.concept.implication_rule = 'unary'
|
248
|
+
entity_type = @entity_type || implicit_boolean_type(role.object_type.vocabulary)
|
249
|
+
phantom_role = @constellation.Role(link_fact_type, 0, :object_type => entity_type, :concept => :new)
|
250
|
+
end
|
251
|
+
|
252
|
+
def reading_preferably_starting_with_role role, negated = false
|
253
|
+
all_reading_by_ordinal.detect do |reading|
|
254
|
+
reading.text =~ /\{\d\}/ and
|
255
|
+
reading.role_sequence.all_role_ref_in_order[$1.to_i].role == role and
|
256
|
+
reading.is_negative == !!negated
|
257
|
+
end || preferred_reading(negated)
|
258
|
+
end
|
259
|
+
|
260
|
+
def all_role_in_order
|
261
|
+
all_role.sort_by{|r| r.ordinal}
|
262
|
+
end
|
263
|
+
|
264
|
+
def compatible_readings types_array
|
265
|
+
all_reading.select do |reading|
|
266
|
+
ok = true
|
267
|
+
reading.role_sequence.all_role_ref_in_order.each_with_index do |rr, i|
|
268
|
+
ok = false unless types_array[i].include?(rr.role.object_type)
|
269
|
+
end
|
270
|
+
ok
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
class Role
|
276
|
+
def describe(highlight = nil)
|
277
|
+
object_type.name + (self == highlight ? "*" : "")
|
278
|
+
end
|
279
|
+
|
280
|
+
# Find any internal uniqueness constraint on this role only
|
281
|
+
def uniqueness_constraint
|
282
|
+
all_role_ref.detect{|rr|
|
283
|
+
rs = rr.role_sequence
|
284
|
+
rs.all_role_ref.size == 1 and
|
285
|
+
rs.all_presence_constraint.each{|pc|
|
286
|
+
return pc if pc.max_frequency == 1
|
287
|
+
}
|
288
|
+
}
|
289
|
+
nil
|
290
|
+
end
|
291
|
+
|
292
|
+
# Is there are internal uniqueness constraint on this role only?
|
293
|
+
def unique
|
294
|
+
uniqueness_constraint ? true : false
|
295
|
+
end
|
296
|
+
|
297
|
+
def is_mandatory
|
298
|
+
return fact_type.implying_role.is_mandatory if fact_type.is_a?(LinkFactType)
|
299
|
+
all_role_ref.detect{|rr|
|
300
|
+
rs = rr.role_sequence
|
301
|
+
rs.all_role_ref.size == 1 and
|
302
|
+
rs.all_presence_constraint.detect{|pc|
|
303
|
+
pc.min_frequency and pc.min_frequency >= 1 and pc.is_mandatory
|
304
|
+
}
|
305
|
+
} ? true : false
|
306
|
+
end
|
307
|
+
|
308
|
+
def preferred_reference
|
309
|
+
fact_type.preferred_reading.role_sequence.all_role_ref.detect{|rr| rr.role == self }
|
310
|
+
end
|
311
|
+
|
312
|
+
# Return true if this role is functional (has only one instance wrt its player)
|
313
|
+
# A role in an objectified fact type is deemed to refer to the implicit role of the objectification.
|
314
|
+
def is_functional
|
315
|
+
fact_type.entity_type or
|
316
|
+
fact_type.all_role.size != 2 or
|
317
|
+
is_unique
|
318
|
+
end
|
319
|
+
|
320
|
+
def is_unique
|
321
|
+
all_role_ref.detect do |rr|
|
322
|
+
rr.role_sequence.all_role_ref.size == 1 and
|
323
|
+
rr.role_sequence.all_presence_constraint.detect do |pc|
|
324
|
+
pc.max_frequency == 1 and !pc.enforcement # Alethic uniqueness constraint
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
def name
|
330
|
+
role_name || object_type.name
|
331
|
+
end
|
332
|
+
|
333
|
+
end
|
334
|
+
|
335
|
+
class RoleRef
|
336
|
+
def describe
|
337
|
+
role_name
|
338
|
+
end
|
339
|
+
|
340
|
+
def preferred_reference
|
341
|
+
role.preferred_reference
|
342
|
+
end
|
343
|
+
|
344
|
+
def role_name(separator = "-")
|
345
|
+
return 'UNKNOWN' unless role
|
346
|
+
name_array =
|
347
|
+
if role.fact_type.all_role.size == 1
|
348
|
+
if role.fact_type.is_a?(LinkFactType)
|
349
|
+
"#{role.object_type.name} phantom for #{role.fact_type.role.object_type.name}"
|
350
|
+
else
|
351
|
+
role.fact_type.preferred_reading.text.gsub(/\{[0-9]\}/,'').strip.split(/\s/)
|
352
|
+
end
|
353
|
+
else
|
354
|
+
role.role_name || [leading_adjective, role.object_type.name, trailing_adjective].compact.map{|w| w.split(/\s/)}.flatten
|
355
|
+
end
|
356
|
+
return separator ? Array(name_array)*separator : Array(name_array)
|
357
|
+
end
|
358
|
+
|
359
|
+
def cql_leading_adjective
|
360
|
+
if leading_adjective
|
361
|
+
# 'foo' => "foo-"
|
362
|
+
# 'foo bar' => "foo- bar "
|
363
|
+
# 'foo-bar' => "foo-- bar "
|
364
|
+
# 'foo-bar baz' => "foo-- bar baz "
|
365
|
+
# 'bat foo-bar baz' => "bat- foo-bar baz "
|
366
|
+
leading_adjective.strip.
|
367
|
+
sub(/[- ]|$/, '-\0 ').sub(/ /, ' ').sub(/[^-]$/, '\0 ').sub(/- $/,'-')
|
368
|
+
else
|
369
|
+
''
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
def cql_trailing_adjective
|
374
|
+
if trailing_adjective
|
375
|
+
# 'foo' => "-foo"
|
376
|
+
# 'foo bar' => " foo -bar"
|
377
|
+
# 'foo-bar' => " foo --bar"
|
378
|
+
# 'foo-bar baz' => " foo-bar -baz"
|
379
|
+
# 'bat foo-bar baz' => " bat foo-bar -baz"
|
380
|
+
trailing_adjective.
|
381
|
+
strip.
|
382
|
+
sub(/(?<a>.*) (?<b>[^- ]+$)|(?<a>.*)(?<b>-[^- ]*)$|(?<a>)(?<b>.*)/) {
|
383
|
+
" #{$~[:a]} -#{$~[:b]}"
|
384
|
+
}.
|
385
|
+
sub(/^ *-/, '-') # A leading space is not needed if the hyphen is at the start
|
386
|
+
else
|
387
|
+
''
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
def cql_name
|
392
|
+
if role.fact_type.all_role.size == 1
|
393
|
+
role_name
|
394
|
+
elsif role.role_name
|
395
|
+
role.role_name
|
396
|
+
else
|
397
|
+
# Where an adjective has multiple words, the hyphen is inserted outside the outermost space, leaving the space
|
398
|
+
cql_leading_adjective +
|
399
|
+
role.object_type.name+
|
400
|
+
cql_trailing_adjective
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
class RoleSequence
|
406
|
+
def describe(highlighted_role_ref = nil)
|
407
|
+
"("+
|
408
|
+
all_role_ref.sort_by{|rr| rr.ordinal}.map{|rr| rr.describe + (highlighted_role_ref == rr ? '*' : '') }*", "+
|
409
|
+
")"
|
410
|
+
end
|
411
|
+
|
412
|
+
def all_role_ref_in_order
|
413
|
+
all_role_ref.sort_by{|rr| rr.ordinal}
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
class ObjectType
|
418
|
+
# Placeholder for the surrogate transform
|
419
|
+
attr_reader :injected_surrogate_role
|
420
|
+
|
421
|
+
def is_separate
|
422
|
+
is_independent or concept.all_concept_annotation.detect{|ca| ca.mapping_annotation == 'separate'}
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
class ValueType
|
427
|
+
def supertypes_transitive
|
428
|
+
[self] + (supertype ? supertype.supertypes_transitive : [])
|
429
|
+
end
|
430
|
+
|
431
|
+
def subtypes
|
432
|
+
all_value_type_as_supertype
|
433
|
+
end
|
434
|
+
|
435
|
+
def subtypes_transitive
|
436
|
+
[self] + subtypes.map{|st| st.subtypes_transitive}.flatten
|
437
|
+
end
|
438
|
+
|
439
|
+
def common_supertype(other)
|
440
|
+
return nil unless other.is_?(ActiveFacts::Metamodel::ValueType)
|
441
|
+
return self if other.supertypes_transitive.include?(self)
|
442
|
+
return other if supertypes_transitive.include(other)
|
443
|
+
nil
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
class EntityType
|
448
|
+
def identification_is_inherited
|
449
|
+
preferred_identifier and
|
450
|
+
preferred_identifier.role_sequence.all_role_ref.detect{|rr| rr.role.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance) }
|
451
|
+
end
|
452
|
+
|
453
|
+
def assimilation
|
454
|
+
if rr = identification_is_inherited
|
455
|
+
rr.role.fact_type.assimilation
|
456
|
+
else
|
457
|
+
nil
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
def preferred_identifier
|
462
|
+
return @preferred_identifier if @preferred_identifier
|
463
|
+
if fact_type
|
464
|
+
# When compiling a fact instance, the delayed creation of a preferred identifier might be necessary
|
465
|
+
if c = fact_type.check_and_add_spanning_uniqueness_constraint
|
466
|
+
fact_type.check_and_add_spanning_uniqueness_constraint = nil
|
467
|
+
c.call
|
468
|
+
end
|
469
|
+
|
470
|
+
# For a nested fact type, the PI is a unique constraint over N or N-1 roles
|
471
|
+
fact_roles = Array(fact_type.all_role)
|
472
|
+
trace :pi, "Looking for PI on nested fact type #{name}" do
|
473
|
+
pi = catch :pi do
|
474
|
+
fact_roles[0,2].each{|r| # Try the first two roles of the fact type, that's enough
|
475
|
+
r.all_role_ref.map{|rr| # All role sequences that reference this role
|
476
|
+
role_sequence = rr.role_sequence
|
477
|
+
|
478
|
+
# The role sequence is only interesting if it cover only this fact's roles
|
479
|
+
# or roles of the objectification
|
480
|
+
next if role_sequence.all_role_ref.size < fact_roles.size-1 # Not enough roles
|
481
|
+
next if role_sequence.all_role_ref.size > fact_roles.size # Too many roles
|
482
|
+
next if role_sequence.all_role_ref.detect do |rsr|
|
483
|
+
if (of = rsr.role.fact_type) != fact_type
|
484
|
+
case of.all_role.size
|
485
|
+
when 1 # A unary FT must be played by the objectification of this fact type
|
486
|
+
next rsr.role.object_type != fact_type.entity_type
|
487
|
+
when 2 # A binary FT must have the objectification of this FT as the other player
|
488
|
+
other_role = (of.all_role-[rsr.role])[0]
|
489
|
+
next other_role.object_type != fact_type.entity_type
|
490
|
+
else
|
491
|
+
next true # A role in a ternary (or higher) cannot be usd in our identifier
|
492
|
+
end
|
493
|
+
end
|
494
|
+
rsr.role.fact_type != fact_type
|
495
|
+
end
|
496
|
+
|
497
|
+
# This role sequence is a candidate
|
498
|
+
pc = role_sequence.all_presence_constraint.detect{|c|
|
499
|
+
c.max_frequency == 1 && c.is_preferred_identifier
|
500
|
+
}
|
501
|
+
throw :pi, pc if pc
|
502
|
+
}
|
503
|
+
}
|
504
|
+
throw :pi, nil
|
505
|
+
end
|
506
|
+
trace :pi, "Got PI #{pi.name||pi.object_id} for nested #{name}" if pi
|
507
|
+
trace :pi, "Looking for PI on entity that nests this fact" unless pi
|
508
|
+
raise "Oops, pi for nested fact is #{pi.class}" unless !pi || pi.is_a?(ActiveFacts::Metamodel::PresenceConstraint)
|
509
|
+
return @preferred_identifier = pi if pi
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
trace :pi, "Looking for PI for ordinary entity #{name} with #{all_role.size} roles:" do
|
514
|
+
trace :pi, "Roles are in fact types #{all_role.map{|r| r.fact_type.describe(r)}*", "}"
|
515
|
+
pi = catch :pi do
|
516
|
+
all_supertypes = supertypes_transitive
|
517
|
+
trace :pi, "PI roles must be played by one of #{all_supertypes.map(&:name)*", "}" if all_supertypes.size > 1
|
518
|
+
all_role.each{|role|
|
519
|
+
next unless role.unique || fact_type
|
520
|
+
ftroles = Array(role.fact_type.all_role)
|
521
|
+
|
522
|
+
# Skip roles in ternary and higher fact types, they're objectified, and in unaries, they can't identify us.
|
523
|
+
next if ftroles.size != 2
|
524
|
+
|
525
|
+
trace :pi, "Considering role in #{role.fact_type.describe(role)}"
|
526
|
+
|
527
|
+
# Find the related role which must be included in any PI:
|
528
|
+
# Note this works with unary fact types:
|
529
|
+
pi_role = ftroles[ftroles[0] != role ? 0 : -1]
|
530
|
+
|
531
|
+
next if ftroles.size == 2 && pi_role.object_type == self
|
532
|
+
trace :pi, " Considering #{pi_role.object_type.name} as a PI role"
|
533
|
+
|
534
|
+
# If this is an identifying role, the PI is a PC whose role_sequence spans the role.
|
535
|
+
# Walk through all role_sequences that span this role, and test each:
|
536
|
+
pi_role.all_role_ref.each{|rr|
|
537
|
+
role_sequence = rr.role_sequence # A role sequence that includes a possible role
|
538
|
+
|
539
|
+
trace :pi, " Considering role sequence #{role_sequence.describe}"
|
540
|
+
|
541
|
+
# All roles in this role_sequence must be in fact types which
|
542
|
+
# (apart from that role) only have roles played by the original
|
543
|
+
# entity type or a supertype.
|
544
|
+
#trace :pi, " All supertypes #{all_supertypes.map{|st| "#{st.object_id}=>#{st.name}"}*", "}"
|
545
|
+
if role_sequence.all_role_ref.detect{|rsr|
|
546
|
+
fact_type = rsr.role.fact_type
|
547
|
+
trace :pi, " Role Sequence touches #{fact_type.describe(pi_role)}"
|
548
|
+
|
549
|
+
fact_type_roles = fact_type.all_role
|
550
|
+
trace :pi, " residual is #{fact_type_roles.map{|r| r.object_type.name}.inspect} minus #{rsr.role.object_type.name}"
|
551
|
+
residual_roles = fact_type_roles-[rsr.role]
|
552
|
+
residual_roles.detect{|rfr|
|
553
|
+
trace :pi, " Checking residual role #{rfr.object_type.object_id}=>#{rfr.object_type.name}"
|
554
|
+
# This next line looks right, but breaks things. Find out what and why:
|
555
|
+
# !rfr.unique or
|
556
|
+
!all_supertypes.include?(rfr.object_type)
|
557
|
+
}
|
558
|
+
}
|
559
|
+
trace :pi, " Discounting this role_sequence because it includes alien roles"
|
560
|
+
next
|
561
|
+
end
|
562
|
+
|
563
|
+
# Any presence constraint over this role sequence is a candidate
|
564
|
+
rr.role_sequence.all_presence_constraint.detect{|pc|
|
565
|
+
# Found it!
|
566
|
+
if pc.is_preferred_identifier
|
567
|
+
trace :pi, "found PI #{pc.name||pc.object_id}, is_preferred_identifier=#{pc.is_preferred_identifier.inspect} over #{pc.role_sequence.describe}"
|
568
|
+
throw :pi, pc
|
569
|
+
end
|
570
|
+
}
|
571
|
+
}
|
572
|
+
}
|
573
|
+
throw :pi, nil
|
574
|
+
end
|
575
|
+
raise "Oops, pi for entity is #{pi.class}" if pi && !pi.is_a?(ActiveFacts::Metamodel::PresenceConstraint)
|
576
|
+
trace :pi, "Got PI #{pi.name||pi.object_id} for #{name}" if pi
|
577
|
+
|
578
|
+
if !pi
|
579
|
+
if (supertype = identifying_supertype)
|
580
|
+
# This shouldn't happen now, as an identifying supertype is connected by a fact type
|
581
|
+
# that has a uniqueness constraint marked as the preferred identifier.
|
582
|
+
#trace :pi, "PI not found for #{name}, looking in supertype #{supertype.name}"
|
583
|
+
#pi = supertype.preferred_identifier
|
584
|
+
#return nil
|
585
|
+
elsif fact_type
|
586
|
+
possible_pi = nil
|
587
|
+
fact_type.all_role.each{|role|
|
588
|
+
role.all_role_ref.each{|role_ref|
|
589
|
+
# Discount role sequences that contain roles not in this fact type:
|
590
|
+
next if role_ref.role_sequence.all_role_ref.detect{|rr| rr.role.fact_type != fact_type }
|
591
|
+
role_ref.role_sequence.all_presence_constraint.each{|pc|
|
592
|
+
next unless pc.max_frequency == 1
|
593
|
+
possible_pi = pc
|
594
|
+
next unless pc.is_preferred_identifier
|
595
|
+
pi = pc
|
596
|
+
break
|
597
|
+
}
|
598
|
+
break if pi
|
599
|
+
}
|
600
|
+
break if pi
|
601
|
+
}
|
602
|
+
if !pi && possible_pi
|
603
|
+
trace :pi, "Using existing PC as PI for #{name}"
|
604
|
+
pi = possible_pi
|
605
|
+
end
|
606
|
+
else
|
607
|
+
byebug
|
608
|
+
trace :pi, "No PI found for #{name}"
|
609
|
+
end
|
610
|
+
end
|
611
|
+
raise "No PI found for #{name}" unless pi
|
612
|
+
@preferred_identifier = pi
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
# An array of all direct subtypes:
|
617
|
+
def subtypes
|
618
|
+
# REVISIT: There's no sorting here. Should there be?
|
619
|
+
all_type_inheritance_as_supertype.map{|ti| ti.subtype }
|
620
|
+
end
|
621
|
+
|
622
|
+
def subtypes_transitive
|
623
|
+
[self] + subtypes.map{|st| st.subtypes_transitive}.flatten.uniq
|
624
|
+
end
|
625
|
+
|
626
|
+
def all_supertype_inheritance
|
627
|
+
all_type_inheritance_as_subtype.sort_by{|ti|
|
628
|
+
[ti.provides_identification ? 0 : 1, ti.supertype.name]
|
629
|
+
}
|
630
|
+
end
|
631
|
+
|
632
|
+
# An array all direct supertypes
|
633
|
+
def supertypes
|
634
|
+
all_supertype_inheritance.map{|ti|
|
635
|
+
ti.supertype
|
636
|
+
}
|
637
|
+
end
|
638
|
+
|
639
|
+
# An array of self followed by all supertypes in order:
|
640
|
+
def supertypes_transitive
|
641
|
+
([self] + all_type_inheritance_as_subtype.map{|ti|
|
642
|
+
ti.supertype.supertypes_transitive
|
643
|
+
}).flatten.uniq
|
644
|
+
end
|
645
|
+
|
646
|
+
# A subtype does not have a identifying_supertype if it defines its own identifier
|
647
|
+
def identifying_supertype
|
648
|
+
trace "Looking for identifying_supertype of #{name}"
|
649
|
+
all_type_inheritance_as_subtype.detect{|ti|
|
650
|
+
trace "considering supertype #{ti.supertype.name}"
|
651
|
+
next unless ti.provides_identification
|
652
|
+
trace "found identifying supertype of #{name}, it's #{ti.supertype.name}"
|
653
|
+
return ti.supertype
|
654
|
+
}
|
655
|
+
trace "Failed to find identifying supertype of #{name}"
|
656
|
+
return nil
|
657
|
+
end
|
658
|
+
|
659
|
+
def common_supertype(other)
|
660
|
+
return nil unless other.is_?(ActiveFacts::Metamodel::EntityType)
|
661
|
+
candidates = supertypes_transitive & other.supertypes_transitive
|
662
|
+
return candidates[0] if candidates.size <= 1
|
663
|
+
candidates[0] # REVISIT: This might not be the closest supertype
|
664
|
+
end
|
665
|
+
|
666
|
+
# This entity type has just objectified a fact type. Create the necessary ImplicitFactTypes with phantom roles
|
667
|
+
def create_implicit_fact_types
|
668
|
+
fact_type.all_role.map do |role|
|
669
|
+
next if role.link_fact_type # Already exists
|
670
|
+
link_fact_type = @constellation.LinkFactType(:new, :implying_role => role)
|
671
|
+
link_fact_type.concept.implication_rule = 'objectification'
|
672
|
+
phantom_role = @constellation.Role(link_fact_type, 0, :object_type => self, :concept => :new)
|
673
|
+
# We could create a copy of the visible external role here, but there's no need yet...
|
674
|
+
# Nor is there a need for a presence constraint, readings, etc.
|
675
|
+
link_fact_type
|
676
|
+
end
|
677
|
+
end
|
678
|
+
end
|
679
|
+
|
680
|
+
class Reading
|
681
|
+
# The frequency_constraints array here, if supplied, may provide for each role either:
|
682
|
+
# * a PresenceConstraint to be verbalised against the relevant role, or
|
683
|
+
# * a String, used as a definite or indefinite article on the relevant role, or
|
684
|
+
# * an array containing two strings (an article and a super-type name)
|
685
|
+
# The order in the array is the same as the reading's role-sequence.
|
686
|
+
# REVISIT: This should probably be changed to be the fact role sequence.
|
687
|
+
#
|
688
|
+
# define_role_names here is false (use defined names), true (define names) or nil (neither)
|
689
|
+
def expand(frequency_constraints = [], define_role_names = nil, literals = [], &subscript_block)
|
690
|
+
expanded = "#{text}"
|
691
|
+
role_refs = role_sequence.all_role_ref.sort_by{|role_ref| role_ref.ordinal}
|
692
|
+
(0...role_refs.size).each{|i|
|
693
|
+
role_ref = role_refs[i]
|
694
|
+
role = role_ref.role
|
695
|
+
l_adj = "#{role_ref.leading_adjective}".sub(/(\b-\b|.\b|.\Z)/, '\1-').sub(/\b--\b/,'-- ').sub(/- /,'- ')
|
696
|
+
l_adj = nil if l_adj == ""
|
697
|
+
# Double the space to compensate for space removed below
|
698
|
+
# REVISIT: hyphenated trailing adjectives are not correctly represented here
|
699
|
+
t_adj = "#{role_ref.trailing_adjective}".sub(/(\b.|\A.)/, '-\1').sub(/ -/,' -')
|
700
|
+
t_adj = nil if t_adj == ""
|
701
|
+
|
702
|
+
expanded.gsub!(/\{#{i}\}/) do
|
703
|
+
role_ref = role_refs[i]
|
704
|
+
if role_ref.role
|
705
|
+
player = role_ref.role.object_type
|
706
|
+
role_name = role.role_name
|
707
|
+
role_name = nil if role_name == ""
|
708
|
+
if role_name && define_role_names == false
|
709
|
+
l_adj = t_adj = nil # When using role names, don't add adjectives
|
710
|
+
end
|
711
|
+
|
712
|
+
freq_con = frequency_constraints[i]
|
713
|
+
freq_con = freq_con.frequency if freq_con && freq_con.is_a?(ActiveFacts::Metamodel::PresenceConstraint)
|
714
|
+
if freq_con.is_a?(Array)
|
715
|
+
freq_con, player_name = *freq_con
|
716
|
+
else
|
717
|
+
player_name = player.name
|
718
|
+
end
|
719
|
+
else
|
720
|
+
# We have an unknown role. The reading cannot be correctly expanded
|
721
|
+
player_name = "UNKNOWN"
|
722
|
+
role_name = nil
|
723
|
+
freq_con = nil
|
724
|
+
end
|
725
|
+
|
726
|
+
literal = literals[i]
|
727
|
+
words = [
|
728
|
+
freq_con ? freq_con : nil,
|
729
|
+
l_adj,
|
730
|
+
define_role_names == false && role_name ? role_name : player_name,
|
731
|
+
t_adj,
|
732
|
+
define_role_names && role_name && player_name != role_name ? "(as #{role_name})" : nil,
|
733
|
+
# Can't have both a literal and a value constraint, but we don't enforce that here:
|
734
|
+
literal ? literal : nil
|
735
|
+
]
|
736
|
+
if (subscript_block)
|
737
|
+
words = subscript_block.call(role_ref, *words)
|
738
|
+
end
|
739
|
+
words.compact*" "
|
740
|
+
end
|
741
|
+
}
|
742
|
+
expanded.gsub!(/ ?- ?/, '-') # Remove single spaces around adjectives
|
743
|
+
#trace "Expanded '#{expanded}' using #{frequency_constraints.inspect}"
|
744
|
+
expanded
|
745
|
+
end
|
746
|
+
|
747
|
+
def words_and_role_refs
|
748
|
+
text.
|
749
|
+
scan(/(?: |\{[0-9]+\}|[^{} ]+)/). # split up the text into words
|
750
|
+
reject{|s| s==' '}. # Remove white space
|
751
|
+
map do |frag| # and go through the bits
|
752
|
+
if frag =~ /\{([0-9]+)\}/
|
753
|
+
role_sequence.all_role_ref.detect{|rr| rr.ordinal == $1.to_i}
|
754
|
+
else
|
755
|
+
frag
|
756
|
+
end
|
757
|
+
end
|
758
|
+
end
|
759
|
+
|
760
|
+
# Return the array of the numbers of the RoleRefs inserted into this reading from the role_sequence
|
761
|
+
def role_numbers
|
762
|
+
text.scan(/\{(\d)\}/).flatten.map{|m| Integer(m) }
|
763
|
+
end
|
764
|
+
|
765
|
+
def expand_with_final_presence_constraint &b
|
766
|
+
# Arrange the roles in order they occur in this reading:
|
767
|
+
role_refs = role_sequence.all_role_ref_in_order
|
768
|
+
role_numbers = text.scan(/\{(\d)\}/).flatten.map{|m| Integer(m) }
|
769
|
+
roles = role_numbers.map{|m| role_refs[m].role }
|
770
|
+
fact_constraints = fact_type.internal_presence_constraints
|
771
|
+
|
772
|
+
# Find the constraints that constrain frequency over each role we can verbalise:
|
773
|
+
frequency_constraints = []
|
774
|
+
roles.each do |role|
|
775
|
+
frequency_constraints <<
|
776
|
+
if (role == roles.last) # On the last role of the reading, emit any presence constraint
|
777
|
+
constraint = fact_constraints.
|
778
|
+
detect do |c| # Find a UC that spans all other Roles
|
779
|
+
c.is_a?(ActiveFacts::Metamodel::PresenceConstraint) &&
|
780
|
+
roles-c.role_sequence.all_role_ref.map(&:role) == [role]
|
781
|
+
end
|
782
|
+
constraint && constraint.frequency
|
783
|
+
else
|
784
|
+
nil
|
785
|
+
end
|
786
|
+
end
|
787
|
+
|
788
|
+
expand(frequency_constraints) { |*a| b && b.call(*a) }
|
789
|
+
end
|
790
|
+
end
|
791
|
+
|
792
|
+
class ValueConstraint
|
793
|
+
def describe
|
794
|
+
as_cql
|
795
|
+
end
|
796
|
+
|
797
|
+
def as_cql
|
798
|
+
"restricted to "+
|
799
|
+
( if regular_expression
|
800
|
+
'/' + regular_expression + '/'
|
801
|
+
else
|
802
|
+
'{' + all_allowed_range_sorted.map{|ar| ar.to_s(false) }*', ' + '}'
|
803
|
+
end
|
804
|
+
)
|
805
|
+
end
|
806
|
+
|
807
|
+
def all_allowed_range_sorted
|
808
|
+
all_allowed_range.sort_by{|ar|
|
809
|
+
((min = ar.value_range.minimum_bound) && min.value.literal) ||
|
810
|
+
((max = ar.value_range.maximum_bound) && max.value.literal)
|
811
|
+
}
|
812
|
+
end
|
813
|
+
|
814
|
+
def to_s
|
815
|
+
if all_allowed_range.size > 1
|
816
|
+
"[" +
|
817
|
+
all_allowed_range_sorted.map { |ar| ar.to_s(true) }*", " +
|
818
|
+
"]"
|
819
|
+
else
|
820
|
+
all_allowed_range.single.to_s
|
821
|
+
end
|
822
|
+
end
|
823
|
+
end
|
824
|
+
|
825
|
+
class AllowedRange
|
826
|
+
def to_s(infinity = true)
|
827
|
+
min = value_range.minimum_bound
|
828
|
+
max = value_range.maximum_bound
|
829
|
+
# Open-ended string ranges will fail in Ruby
|
830
|
+
|
831
|
+
if min = value_range.minimum_bound
|
832
|
+
min = min.value
|
833
|
+
if min.is_literal_string
|
834
|
+
min_literal = min.literal.inspect.gsub(/\A"|"\Z/,"'") # Escape string characters
|
835
|
+
else
|
836
|
+
min_literal = min.literal
|
837
|
+
end
|
838
|
+
else
|
839
|
+
min_literal = infinity ? "-Infinity" : ""
|
840
|
+
end
|
841
|
+
if max = value_range.maximum_bound
|
842
|
+
max = max.value
|
843
|
+
if max.is_literal_string
|
844
|
+
max_literal = max.literal.inspect.gsub(/\A"|"\Z/,"'") # Escape string characters
|
845
|
+
else
|
846
|
+
max_literal = max.literal
|
847
|
+
end
|
848
|
+
else
|
849
|
+
max_literal = infinity ? "Infinity" : ""
|
850
|
+
end
|
851
|
+
|
852
|
+
min_literal +
|
853
|
+
(min_literal != (max&&max_literal) ? (".." + max_literal) : "")
|
854
|
+
end
|
855
|
+
end
|
856
|
+
|
857
|
+
class Value
|
858
|
+
def to_s
|
859
|
+
if is_literal_string
|
860
|
+
"'"+
|
861
|
+
literal.
|
862
|
+
inspect. # Use Ruby's inspect to generate necessary escapes
|
863
|
+
gsub(/\A"|"\Z/,''). # Remove surrounding quotes
|
864
|
+
gsub(/'/, "\\'") + # Escape any single quotes
|
865
|
+
"'"
|
866
|
+
else
|
867
|
+
literal
|
868
|
+
end +
|
869
|
+
(unit ? " " + unit.name : "")
|
870
|
+
end
|
871
|
+
|
872
|
+
def inspect
|
873
|
+
to_s
|
874
|
+
end
|
875
|
+
end
|
876
|
+
|
877
|
+
class PresenceConstraint
|
878
|
+
def frequency
|
879
|
+
min = min_frequency
|
880
|
+
max = max_frequency
|
881
|
+
[
|
882
|
+
((min && min > 0 && min != max) ? "at least #{min == 1 ? "one" : min.to_s}" : nil),
|
883
|
+
((max && min != max) ? "at most #{max == 1 ? "one" : max.to_s}" : nil),
|
884
|
+
((max && min == max) ? "#{max == 1 ? "one" : "exactly "+max.to_s}" : nil)
|
885
|
+
].compact * " and "
|
886
|
+
end
|
887
|
+
|
888
|
+
def describe
|
889
|
+
min = min_frequency
|
890
|
+
max = max_frequency
|
891
|
+
'PresenceConstraint over '+role_sequence.describe + " occurs " + frequency + " time#{(min&&min>1)||(max&&max>1) ? 's' : ''}"
|
892
|
+
end
|
893
|
+
end
|
894
|
+
|
895
|
+
class SubsetConstraint
|
896
|
+
def describe
|
897
|
+
'SubsetConstraint(' +
|
898
|
+
subset_role_sequence.describe
|
899
|
+
' < ' +
|
900
|
+
superset_role_sequence.describe +
|
901
|
+
')'
|
902
|
+
end
|
903
|
+
end
|
904
|
+
|
905
|
+
class SetComparisonConstraint
|
906
|
+
def describe
|
907
|
+
self.class.basename+'(' +
|
908
|
+
all_set_comparison_roles.map do |scr|
|
909
|
+
'['+
|
910
|
+
scr.role_sequence.all_role_ref.map{|rr|
|
911
|
+
rr.role.fact_type.describe(rr.role)
|
912
|
+
}*',' +
|
913
|
+
']'
|
914
|
+
end*',' +
|
915
|
+
')'
|
916
|
+
end
|
917
|
+
end
|
918
|
+
|
919
|
+
class RingConstraint
|
920
|
+
def describe
|
921
|
+
'RingConstraint(' +
|
922
|
+
ring_type.to_s+': ' +
|
923
|
+
role.describe+', ' +
|
924
|
+
other_role.describe+' in ' +
|
925
|
+
role.fact_type.default_reading +
|
926
|
+
')'
|
927
|
+
end
|
928
|
+
end
|
929
|
+
|
930
|
+
class TypeInheritance
|
931
|
+
def describe(role = nil)
|
932
|
+
"#{subtype.name} is a kind of #{supertype.name}"
|
933
|
+
end
|
934
|
+
|
935
|
+
def supertype_role
|
936
|
+
(roles = all_role.to_a)[0].object_type == supertype ? roles[0] : roles[1]
|
937
|
+
end
|
938
|
+
|
939
|
+
def subtype_role
|
940
|
+
(roles = all_role.to_a)[0].object_type == subtype ? roles[0] : roles[1]
|
941
|
+
end
|
942
|
+
end
|
943
|
+
|
944
|
+
class Step
|
945
|
+
def describe
|
946
|
+
"Step " +
|
947
|
+
"#{is_optional ? 'maybe ' : ''}" +
|
948
|
+
(is_unary_step ? '(unary) ' : "from #{input_play.describe} ") +
|
949
|
+
"#{is_disallowed ? 'not ' : ''}" +
|
950
|
+
"to #{output_plays.map(&:describe)*', '}" +
|
951
|
+
(objectification_variable ? ", objectified as #{objectification_variable.describe}" : '') +
|
952
|
+
" '#{fact_type.default_reading}'"
|
953
|
+
end
|
954
|
+
|
955
|
+
def input_play
|
956
|
+
all_play.detect{|p| p.is_input}
|
957
|
+
end
|
958
|
+
|
959
|
+
def output_plays
|
960
|
+
all_play.reject{|p| p.is_input}
|
961
|
+
end
|
962
|
+
|
963
|
+
def is_unary_step
|
964
|
+
# Preserve this in case we have to use a real variable for the phantom
|
965
|
+
all_play.size == 1
|
966
|
+
end
|
967
|
+
|
968
|
+
def is_objectification_step
|
969
|
+
!!objectification_variable
|
970
|
+
end
|
971
|
+
|
972
|
+
def external_fact_type
|
973
|
+
fact_type.is_a?(LinkFactType) ? fact_type.role.fact_type : fact_type
|
974
|
+
end
|
975
|
+
end
|
976
|
+
|
977
|
+
class Variable
|
978
|
+
def describe
|
979
|
+
object_type.name +
|
980
|
+
(subscript ? "(#{subscript})" : '') +
|
981
|
+
" Var#{ordinal}" +
|
982
|
+
(value ? ' = '+value.to_s : '')
|
983
|
+
end
|
984
|
+
|
985
|
+
def all_step
|
986
|
+
all_play.map(&:step).uniq
|
987
|
+
end
|
988
|
+
end
|
989
|
+
|
990
|
+
class Play
|
991
|
+
def describe
|
992
|
+
"#{role.object_type.name} Var#{variable.ordinal}" +
|
993
|
+
(role_ref ? " (projected)" : "")
|
994
|
+
end
|
995
|
+
end
|
996
|
+
|
997
|
+
class Query
|
998
|
+
def describe
|
999
|
+
steps_shown = {}
|
1000
|
+
'Query(' +
|
1001
|
+
all_variable.sort_by{|var| var.ordinal}.map do |variable|
|
1002
|
+
variable.describe + ': ' +
|
1003
|
+
variable.all_step.map do |step|
|
1004
|
+
next if steps_shown[step]
|
1005
|
+
steps_shown[step] = true
|
1006
|
+
step.describe
|
1007
|
+
end.compact.join(',')
|
1008
|
+
end.join('; ') +
|
1009
|
+
')'
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
def show
|
1013
|
+
steps_shown = {}
|
1014
|
+
trace :query, "Displaying full contents of Query #{concept.guid}" do
|
1015
|
+
all_variable.sort_by{|var| var.ordinal}.each do |variable|
|
1016
|
+
trace :query, "#{variable.describe}" do
|
1017
|
+
variable.all_step.
|
1018
|
+
each do |step|
|
1019
|
+
next if steps_shown[step]
|
1020
|
+
steps_shown[step] = true
|
1021
|
+
trace :query, "#{step.describe}"
|
1022
|
+
end
|
1023
|
+
variable.all_play.each do |play|
|
1024
|
+
trace :query, "role of #{play.describe} in '#{play.role.fact_type.default_reading}'"
|
1025
|
+
end
|
1026
|
+
end
|
1027
|
+
end
|
1028
|
+
end
|
1029
|
+
end
|
1030
|
+
|
1031
|
+
def all_step
|
1032
|
+
all_variable.map{|var| var.all_step.to_a}.flatten.uniq
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
# Check all parts of this query for validity
|
1036
|
+
def validate
|
1037
|
+
show
|
1038
|
+
return
|
1039
|
+
|
1040
|
+
# Check the variables:
|
1041
|
+
steps = []
|
1042
|
+
variables = all_variable.sort_by{|var| var.ordinal}
|
1043
|
+
variables.each_with_index do |variable, i|
|
1044
|
+
raise "Variable #{i} should have ordinal #{variable.ordinal}" unless variable.ordinal == i
|
1045
|
+
raise "Variable #{i} has missing object_type" unless variable.object_type
|
1046
|
+
variable.all_play do |play|
|
1047
|
+
raise "Variable for #{object_type.name} includes role played by #{play.object_type.name}" unless play.object_type == object_type
|
1048
|
+
end
|
1049
|
+
steps += variable.all_step
|
1050
|
+
end
|
1051
|
+
steps.uniq!
|
1052
|
+
|
1053
|
+
# Check the steps:
|
1054
|
+
steps.each do |step|
|
1055
|
+
raise "Step has missing fact type" unless step.fact_type
|
1056
|
+
raise "Step has missing input node" unless step.input_play
|
1057
|
+
raise "Step has missing output node" unless step.output_play
|
1058
|
+
if (role = input_play).role.fact_type != fact_type or
|
1059
|
+
(role = output_play).role.fact_type != fact_type
|
1060
|
+
raise "Step has role #{role.describe} which doesn't belong to the fact type '#{fact_type.default_reading}' it traverses"
|
1061
|
+
end
|
1062
|
+
end
|
1063
|
+
|
1064
|
+
# REVISIT: Do a connectivity check
|
1065
|
+
end
|
1066
|
+
end
|
1067
|
+
|
1068
|
+
class LinkFactType
|
1069
|
+
def default_reading
|
1070
|
+
# There are two cases, where role is in a unary fact type, and where the fact type is objectified
|
1071
|
+
# If a unary fact type is objectified, only the LinkFactType for the objectification is asserted
|
1072
|
+
if objectification = implying_role.fact_type.entity_type
|
1073
|
+
"#{objectification.name} involves #{implying_role.object_type.name}"
|
1074
|
+
else
|
1075
|
+
implying_role.fact_type.default_reading+" Boolean" # Must be a unary FT
|
1076
|
+
end
|
1077
|
+
end
|
1078
|
+
|
1079
|
+
def add_reading implicit_reading
|
1080
|
+
@readings ||= []
|
1081
|
+
@readings << implicit_reading
|
1082
|
+
end
|
1083
|
+
|
1084
|
+
def all_reading
|
1085
|
+
@readings ||=
|
1086
|
+
[ ImplicitReading.new(
|
1087
|
+
self,
|
1088
|
+
implying_role.fact_type.entity_type ? "{0} involves {1}" : implying_role.fact_type.default_reading+" Boolean"
|
1089
|
+
)
|
1090
|
+
] +
|
1091
|
+
Array(implying_role.fact_type.entity_type ? ImplicitReading.new(self, "{1} is involved in {0}") : nil)
|
1092
|
+
end
|
1093
|
+
|
1094
|
+
def reading_preferably_starting_with_role role, negated = false
|
1095
|
+
all_reading[role == implying_role ? 1 : 0]
|
1096
|
+
end
|
1097
|
+
|
1098
|
+
# This is only used for debugging, from RoleRef#describe
|
1099
|
+
class ImplicitReading
|
1100
|
+
attr_accessor :fact_type, :text
|
1101
|
+
attr_reader :is_negative # Never true
|
1102
|
+
|
1103
|
+
def initialize(fact_type, text)
|
1104
|
+
@fact_type = fact_type
|
1105
|
+
@text = text
|
1106
|
+
end
|
1107
|
+
|
1108
|
+
class ImplicitReadingRoleSequence
|
1109
|
+
class ImplicitReadingRoleRef
|
1110
|
+
attr_reader :role
|
1111
|
+
attr_reader :role_sequence
|
1112
|
+
def initialize(role, role_sequence)
|
1113
|
+
@role = role
|
1114
|
+
@role_sequence = role_sequence
|
1115
|
+
end
|
1116
|
+
def variable; nil; end
|
1117
|
+
def play; nil; end
|
1118
|
+
def leading_adjective; nil; end
|
1119
|
+
def trailing_adjective; nil; end
|
1120
|
+
def describe
|
1121
|
+
@role.object_type.name
|
1122
|
+
end
|
1123
|
+
end
|
1124
|
+
|
1125
|
+
def initialize roles
|
1126
|
+
@role_refs = roles.map{|role| ImplicitReadingRoleRef.new(role, self) }
|
1127
|
+
end
|
1128
|
+
|
1129
|
+
def all_role_ref
|
1130
|
+
@role_refs
|
1131
|
+
end
|
1132
|
+
def all_role_ref_in_order
|
1133
|
+
@role_refs
|
1134
|
+
end
|
1135
|
+
def describe
|
1136
|
+
'('+@role_refs.map(&:describe)*', '+')'
|
1137
|
+
end
|
1138
|
+
def all_reading
|
1139
|
+
[]
|
1140
|
+
end
|
1141
|
+
end
|
1142
|
+
|
1143
|
+
def role_sequence
|
1144
|
+
ImplicitReadingRoleSequence.new([@fact_type.implying_role, @fact_type.all_role.single])
|
1145
|
+
end
|
1146
|
+
|
1147
|
+
def ordinal; 0; end
|
1148
|
+
|
1149
|
+
def expand
|
1150
|
+
text.gsub(/\{([01])\}/) do
|
1151
|
+
if $1 == '0'
|
1152
|
+
fact_type.all_role[0].object_type.name
|
1153
|
+
else
|
1154
|
+
fact_type.implying_role.object_type.name
|
1155
|
+
end
|
1156
|
+
end
|
1157
|
+
end
|
1158
|
+
end
|
1159
|
+
end
|
1160
|
+
|
1161
|
+
# Some queries must be over the proximate roles, some over the counterpart roles.
|
1162
|
+
# Return the common superclass of the appropriate roles, and the actual roles
|
1163
|
+
def self.plays_over roles, options = :both # Or :proximate, :counterpart
|
1164
|
+
# If we can stay inside this objectified FT, there's no query:
|
1165
|
+
roles = Array(roles) # To be safe, in case we get a role collection proxy
|
1166
|
+
return nil if roles.size == 1 or
|
1167
|
+
options != :counterpart && roles.map{|role| role.fact_type}.uniq.size == 1
|
1168
|
+
proximate_sups, counterpart_sups, obj_sups, counterpart_roles, objectification_roles =
|
1169
|
+
*roles.inject(nil) do |d_c_o, role|
|
1170
|
+
object_type = role.object_type
|
1171
|
+
fact_type = role.fact_type
|
1172
|
+
|
1173
|
+
proximate_role_supertypes = object_type.supertypes_transitive
|
1174
|
+
|
1175
|
+
# A role in an objectified fact type may indicate either the objectification or the counterpart player.
|
1176
|
+
# This could be ambiguous. Figure out both and prefer the counterpart over the objectification.
|
1177
|
+
counterpart_role_supertypes =
|
1178
|
+
if fact_type.all_role.size > 2
|
1179
|
+
possible_roles = fact_type.all_role.select{|r| d_c_o && d_c_o[1].include?(r.object_type) }
|
1180
|
+
if possible_roles.size == 1 # Only one candidate matches the types of the possible variables
|
1181
|
+
counterpart_role = possible_roles[0]
|
1182
|
+
d_c_o[1] # No change
|
1183
|
+
else
|
1184
|
+
# puts "#{constraint_type} #{name}: Awkward, try counterpart-role query on a >2ary '#{fact_type.default_reading}'"
|
1185
|
+
# Try all roles; hopefully we don't have two roles with a matching candidate here:
|
1186
|
+
# Find which role is compatible with the existing supertypes, if any
|
1187
|
+
if d_c_o
|
1188
|
+
st = nil
|
1189
|
+
counterpart_role =
|
1190
|
+
fact_type.all_role.detect{|r| ((st = r.object_type.supertypes_transitive) & d_c_o[1]).size > 0}
|
1191
|
+
st
|
1192
|
+
else
|
1193
|
+
counterpart_role = nil # This can't work, we don't have any basis for a decision (must be objectification)
|
1194
|
+
[]
|
1195
|
+
end
|
1196
|
+
#fact_type.all_role.map{|r| r.object_type.supertypes_transitive}.flatten.uniq
|
1197
|
+
end
|
1198
|
+
else
|
1199
|
+
# Get the supertypes of the counterpart role (care with unaries):
|
1200
|
+
ftr = role.fact_type.all_role.to_a
|
1201
|
+
(counterpart_role = ftr[0] == role ? ftr[-1] : ftr[0]).object_type.supertypes_transitive
|
1202
|
+
end
|
1203
|
+
|
1204
|
+
if fact_type.entity_type
|
1205
|
+
objectification_role_supertypes =
|
1206
|
+
fact_type.entity_type.supertypes_transitive+object_type.supertypes_transitive
|
1207
|
+
objectification_role = role.link_fact_type.all_role.single # Find the phantom role here
|
1208
|
+
else
|
1209
|
+
objectification_role_supertypes = counterpart_role_supertypes
|
1210
|
+
objectification_role = counterpart_role
|
1211
|
+
end
|
1212
|
+
|
1213
|
+
if !d_c_o
|
1214
|
+
d_c_o = [proximate_role_supertypes, counterpart_role_supertypes, objectification_role_supertypes, [counterpart_role], [objectification_role]]
|
1215
|
+
#puts "role player supertypes starts #{d_c_o.map{|dco| dco.map(&:name).inspect}*' or '}"
|
1216
|
+
else
|
1217
|
+
#puts "continues #{[proximate_role_supertypes, counterpart_role_supertypes, objectification_role_supertypes]map{|dco| dco.map(&:name).inspect}*' or '}"
|
1218
|
+
d_c_o[0] &= proximate_role_supertypes
|
1219
|
+
d_c_o[1] &= counterpart_role_supertypes
|
1220
|
+
d_c_o[2] &= objectification_role_supertypes
|
1221
|
+
d_c_o[3] << (counterpart_role || objectification_role)
|
1222
|
+
d_c_o[4] << (objectification_role || counterpart_role)
|
1223
|
+
end
|
1224
|
+
d_c_o
|
1225
|
+
end # inject
|
1226
|
+
|
1227
|
+
# Discount a subtype step over an object type that's not a player here,
|
1228
|
+
# if we can use an objectification step to an object type that is:
|
1229
|
+
if counterpart_sups.size > 0 && obj_sups.size > 0 && counterpart_sups[0] != obj_sups[0]
|
1230
|
+
trace :query, "ambiguous query, could be over #{counterpart_sups[0].name} or #{obj_sups[0].name}"
|
1231
|
+
if !roles.detect{|r| r.object_type == counterpart_sups[0]} and roles.detect{|r| r.object_type == obj_sups[0]}
|
1232
|
+
trace :query, "discounting #{counterpart_sups[0].name} in favour of direct objectification"
|
1233
|
+
counterpart_sups = []
|
1234
|
+
end
|
1235
|
+
end
|
1236
|
+
|
1237
|
+
# Choose the first entry in the first non-empty supertypes list:
|
1238
|
+
if options != :counterpart && proximate_sups[0]
|
1239
|
+
[ proximate_sups[0], roles ]
|
1240
|
+
elsif !counterpart_sups.empty?
|
1241
|
+
[ counterpart_sups[0], counterpart_roles ]
|
1242
|
+
else
|
1243
|
+
[ obj_sups[0], objectification_roles ]
|
1244
|
+
end
|
1245
|
+
end
|
1246
|
+
|
1247
|
+
class Fact
|
1248
|
+
def verbalise(context = nil)
|
1249
|
+
reading = fact_type.preferred_reading
|
1250
|
+
reading_roles = reading.role_sequence.all_role_ref.sort_by{|rr| rr.ordinal}.map{|rr| rr.role }
|
1251
|
+
role_values_in_reading_order = all_role_value.sort_by{|rv| reading_roles.index(rv.role) }
|
1252
|
+
instance_verbalisations = role_values_in_reading_order.map do |rv|
|
1253
|
+
if rv.instance.value
|
1254
|
+
v = rv.instance.verbalise
|
1255
|
+
else
|
1256
|
+
if (c = rv.instance.object_type).is_a?(EntityType)
|
1257
|
+
if !c.preferred_identifier.role_sequence.all_role_ref.detect{|rr| rr.role.fact_type == fact_type}
|
1258
|
+
v = rv.instance.verbalise
|
1259
|
+
end
|
1260
|
+
end
|
1261
|
+
end
|
1262
|
+
next nil unless v
|
1263
|
+
v.to_s.sub(/(#{rv.instance.object_type.name}|\S*)\s/,'')
|
1264
|
+
end
|
1265
|
+
reading.expand([], false, instance_verbalisations)
|
1266
|
+
end
|
1267
|
+
end
|
1268
|
+
|
1269
|
+
class Instance
|
1270
|
+
def verbalise(context = nil)
|
1271
|
+
return "#{object_type.name} #{value}" if object_type.is_a?(ValueType)
|
1272
|
+
|
1273
|
+
return "#{object_type.name} (in which #{fact.verbalise(context)})" if object_type.fact_type
|
1274
|
+
|
1275
|
+
# It's an entity that's not an objectified fact type
|
1276
|
+
|
1277
|
+
# If it has a simple identifier, there's no need to fully verbalise the identifying facts.
|
1278
|
+
# This recursive block returns either the identifying value or nil
|
1279
|
+
simple_identifier = proc do |instance|
|
1280
|
+
if instance.object_type.is_a?(ActiveFacts::Metamodel::ValueType)
|
1281
|
+
instance
|
1282
|
+
else
|
1283
|
+
pi = instance.object_type.preferred_identifier
|
1284
|
+
identifying_role_refs = pi.role_sequence.all_role_ref_in_order
|
1285
|
+
if identifying_role_refs.size != 1
|
1286
|
+
nil
|
1287
|
+
else
|
1288
|
+
role = identifying_role_refs[0].role
|
1289
|
+
my_role = (role.fact_type.all_role.to_a-[role])[0]
|
1290
|
+
identifying_fact = my_role.all_role_value.detect{|rv| rv.instance == self}.fact
|
1291
|
+
irv = identifying_fact.all_role_value.detect{|rv| rv.role == role}
|
1292
|
+
identifying_instance = irv.instance
|
1293
|
+
simple_identifier.call(identifying_instance)
|
1294
|
+
end
|
1295
|
+
end
|
1296
|
+
end
|
1297
|
+
|
1298
|
+
if (id = simple_identifier.call(self))
|
1299
|
+
"#{object_type.name} #{id.value}"
|
1300
|
+
else
|
1301
|
+
pi = object_type.preferred_identifier
|
1302
|
+
identifying_role_refs = pi.role_sequence.all_role_ref_in_order
|
1303
|
+
"#{object_type.name}" +
|
1304
|
+
" is identified by " + # REVISIT: Where the single fact type is TypeInheritance, we can shrink this
|
1305
|
+
identifying_role_refs.map do |rr|
|
1306
|
+
rr = rr.preferred_reference
|
1307
|
+
[ (l = rr.leading_adjective) ? l+"-" : nil,
|
1308
|
+
rr.role.role_name || rr.role.object_type.name,
|
1309
|
+
(t = rr.trailing_adjective) ? l+"-" : nil
|
1310
|
+
].compact*""
|
1311
|
+
end * " and " +
|
1312
|
+
" where " +
|
1313
|
+
identifying_role_refs.map do |rr| # Go through the identifying roles and emit the facts that define them
|
1314
|
+
instance_role = object_type.all_role.detect{|r| r.fact_type == rr.role.fact_type}
|
1315
|
+
identifying_fact = all_role_value.detect{|rv| rv.fact.fact_type == rr.role.fact_type}.fact
|
1316
|
+
#counterpart_role = (rr.role.fact_type.all_role.to_a-[instance_role])[0]
|
1317
|
+
#identifying_instance = counterpart_role.all_role_value.detect{|rv| rv.fact == identifying_fact}.instance
|
1318
|
+
identifying_fact.verbalise(context)
|
1319
|
+
end*", "
|
1320
|
+
end
|
1321
|
+
|
1322
|
+
end
|
1323
|
+
end
|
1324
|
+
|
1325
|
+
class ContextNote
|
1326
|
+
def verbalise(context=nil)
|
1327
|
+
as_cql
|
1328
|
+
end
|
1329
|
+
|
1330
|
+
def as_cql
|
1331
|
+
' (' +
|
1332
|
+
( if all_context_according_to
|
1333
|
+
'according to '
|
1334
|
+
all_context_according_to.map do |act|
|
1335
|
+
act.agent.agent_name+', '
|
1336
|
+
end.join('')
|
1337
|
+
end
|
1338
|
+
) +
|
1339
|
+
context_note_kind.gsub(/_/, ' ') +
|
1340
|
+
' ' +
|
1341
|
+
discussion +
|
1342
|
+
( if agreement
|
1343
|
+
', as agreed ' +
|
1344
|
+
(agreement.date ? ' on '+agreement.date.iso8601.inspect+' ' : '') +
|
1345
|
+
'by '
|
1346
|
+
agreement.all_context_agreed_by.map do |acab|
|
1347
|
+
acab.agent.agent_name+', '
|
1348
|
+
end.join('')
|
1349
|
+
else
|
1350
|
+
''
|
1351
|
+
end
|
1352
|
+
) +
|
1353
|
+
')'
|
1354
|
+
end
|
1355
|
+
end
|
1356
|
+
|
1357
|
+
end
|
1358
|
+
end
|