activefacts 0.7.3 → 0.8.5
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.
- data/LICENSE +19 -0
- data/Manifest.txt +24 -2
- data/Rakefile +25 -3
- data/bin/afgen +1 -1
- data/bin/cql +13 -2
- data/css/offline.css +3 -0
- data/css/orm2.css +24 -0
- data/css/print.css +8 -0
- data/css/style-print.css +357 -0
- data/css/style.css +387 -0
- data/download.html +85 -0
- data/examples/CQL/Address.cql +3 -3
- data/examples/CQL/Blog.cql +13 -14
- data/examples/CQL/CompanyDirectorEmployee.cql +4 -4
- data/examples/CQL/Death.cql +3 -2
- data/examples/CQL/Genealogy.cql +13 -11
- data/examples/CQL/Marriage.cql +2 -2
- data/examples/CQL/Metamodel.cql +136 -93
- data/examples/CQL/MultiInheritance.cql +2 -2
- data/examples/CQL/OilSupply.cql +14 -10
- data/examples/CQL/Orienteering.cql +22 -19
- data/examples/CQL/PersonPlaysGame.cql +3 -2
- data/examples/CQL/SchoolActivities.cql +4 -2
- data/examples/CQL/SimplestUnary.cql +1 -1
- data/examples/CQL/SubtypePI.cql +6 -7
- data/examples/CQL/Warehousing.cql +16 -19
- data/examples/CQL/unit.cql +584 -0
- data/examples/index.html +276 -0
- data/examples/intro.html +497 -0
- data/examples/local.css +20 -0
- data/index.html +96 -0
- data/lib/activefacts/api/concept.rb +48 -46
- data/lib/activefacts/api/constellation.rb +43 -23
- data/lib/activefacts/api/entity.rb +2 -2
- data/lib/activefacts/api/instance.rb +6 -2
- data/lib/activefacts/api/instance_index.rb +5 -0
- data/lib/activefacts/api/value.rb +8 -2
- data/lib/activefacts/api/vocabulary.rb +15 -10
- data/lib/activefacts/cql/CQLParser.treetop +109 -88
- data/lib/activefacts/cql/Concepts.treetop +32 -10
- data/lib/activefacts/cql/Context.treetop +34 -0
- data/lib/activefacts/cql/Expressions.treetop +9 -9
- data/lib/activefacts/cql/FactTypes.treetop +30 -31
- data/lib/activefacts/cql/Language/English.treetop +50 -0
- data/lib/activefacts/cql/LexicalRules.treetop +2 -1
- data/lib/activefacts/cql/Terms.treetop +117 -0
- data/lib/activefacts/cql/ValueTypes.treetop +152 -0
- data/lib/activefacts/cql/compiler.rb +1718 -0
- data/lib/activefacts/cql/parser.rb +124 -57
- data/lib/activefacts/generate/absorption.rb +1 -1
- data/lib/activefacts/generate/cql.rb +111 -100
- data/lib/activefacts/generate/cql/html.rb +5 -5
- data/lib/activefacts/generate/oo.rb +3 -3
- data/lib/activefacts/generate/ordered.rb +51 -19
- data/lib/activefacts/generate/ruby.rb +10 -8
- data/lib/activefacts/generate/sql/mysql.rb +14 -10
- data/lib/activefacts/generate/sql/server.rb +29 -24
- data/lib/activefacts/input/cql.rb +9 -1264
- data/lib/activefacts/input/orm.rb +213 -200
- data/lib/activefacts/persistence/columns.rb +11 -10
- data/lib/activefacts/persistence/index.rb +15 -18
- data/lib/activefacts/persistence/reference.rb +17 -17
- data/lib/activefacts/persistence/tables.rb +50 -51
- data/lib/activefacts/version.rb +1 -1
- data/lib/activefacts/vocabulary/extensions.rb +79 -8
- data/lib/activefacts/vocabulary/metamodel.rb +183 -114
- data/spec/absorption_ruby_spec.rb +99 -0
- data/spec/absorption_spec.rb +3 -4
- data/spec/api/constellation.rb +1 -1
- data/spec/api/entity_type.rb +3 -1
- data/spec/api/instance.rb +4 -2
- data/spec/api/roles.rb +8 -6
- data/spec/api_spec.rb +1 -2
- data/spec/cql/context_spec.rb +71 -0
- data/spec/cql/samples_spec.rb +154 -0
- data/spec/cql/unit_spec.rb +375 -0
- data/spec/cql_cql_spec.rb +31 -21
- data/spec/cql_mysql_spec.rb +70 -0
- data/spec/cql_parse_spec.rb +15 -9
- data/spec/cql_ruby_spec.rb +27 -13
- data/spec/cql_sql_spec.rb +42 -16
- data/spec/cql_symbol_tables_spec.rb +2 -3
- data/spec/cqldump_spec.rb +7 -7
- data/spec/helpers/file_matcher.rb +39 -0
- data/spec/norma_cql_spec.rb +20 -12
- data/spec/norma_ruby_spec.rb +6 -3
- data/spec/norma_sql_spec.rb +6 -3
- data/spec/norma_tables_spec.rb +6 -4
- data/spec/spec_helper.rb +27 -8
- data/status.html +69 -0
- data/why.html +60 -0
- metadata +34 -11
- data/lib/activefacts/cql/DataTypes.treetop +0 -81
- data/spec/cql_unit_spec.rb +0 -330
@@ -92,6 +92,7 @@ module ActiveFacts
|
|
92
92
|
# When Xyz is followed by XyzID, truncate that to just ID
|
93
93
|
debug :columns, "truncating repeated #{last_names.last} in #{names[0]}"
|
94
94
|
names[0] = names[0][last_names.last.size..-1]
|
95
|
+
names.shift if names[0] == ''
|
95
96
|
elsif last_names.last == names[0]
|
96
97
|
# Same, but where an underscore split up the words
|
97
98
|
debug :columns, "truncating repeated name in #{names.inspect}"
|
@@ -358,8 +359,8 @@ module ActiveFacts
|
|
358
359
|
# Make schema transformations like adding ValueType self-value columns (and later, Rails-friendly ID fields).
|
359
360
|
# Override this method to change the transformations
|
360
361
|
def finish_schema
|
361
|
-
|
362
|
-
|
362
|
+
all_concept.each do |concept|
|
363
|
+
concept.self_value_reference if concept.is_a?(ActiveFacts::Metamodel::ValueType) && concept.is_table
|
363
364
|
end
|
364
365
|
end
|
365
366
|
|
@@ -368,18 +369,18 @@ module ActiveFacts
|
|
368
369
|
finish_schema
|
369
370
|
|
370
371
|
debug :columns, "Populating all columns" do
|
371
|
-
|
372
|
-
next if !
|
373
|
-
debug :columns, "Populating columns for table #{
|
374
|
-
|
372
|
+
all_concept.each do |concept|
|
373
|
+
next if !concept.is_table
|
374
|
+
debug :columns, "Populating columns for table #{concept.name}" do
|
375
|
+
concept.populate_columns
|
375
376
|
end
|
376
377
|
end
|
377
378
|
end
|
378
379
|
debug :columns, "Finished columns" do
|
379
|
-
|
380
|
-
next if !
|
381
|
-
debug :columns, "Finished columns for table #{
|
382
|
-
|
380
|
+
all_concept.each do |concept|
|
381
|
+
next if !concept.is_table
|
382
|
+
debug :columns, "Finished columns for table #{concept.name}" do
|
383
|
+
concept.columns.each do |column|
|
383
384
|
debug :columns, "#{column}"
|
384
385
|
end
|
385
386
|
end
|
@@ -54,13 +54,13 @@ module ActiveFacts
|
|
54
54
|
end
|
55
55
|
|
56
56
|
# An array of the names of the columns this index covers
|
57
|
-
def column_names
|
58
|
-
columns.map{|column| column.name}
|
57
|
+
def column_names(joiner = "")
|
58
|
+
columns.map{|column| column.name(joiner)}
|
59
59
|
end
|
60
60
|
|
61
61
|
# An array of the names of the columns this index covers, with some lexical truncations.
|
62
|
-
def abbreviated_column_names
|
63
|
-
columns.map{|column| column.name.sub(/^#{over.name}/,'')}
|
62
|
+
def abbreviated_column_names(joiner = "")
|
63
|
+
columns.map{|column| column.name(joiner).sub(/^#{over.name}/,'')}
|
64
64
|
end
|
65
65
|
|
66
66
|
# The name of a view that can be created to enforce uniqueness over non-null key values
|
@@ -183,25 +183,22 @@ module ActiveFacts
|
|
183
183
|
class Vocabulary
|
184
184
|
def populate_all_indices #:nodoc:
|
185
185
|
debug :index, "Populating all concept indices" do
|
186
|
-
|
187
|
-
|
188
|
-
feature.clear_indices
|
186
|
+
all_concept.each do |concept|
|
187
|
+
concept.clear_indices
|
189
188
|
end
|
190
|
-
|
191
|
-
next unless
|
192
|
-
|
193
|
-
|
194
|
-
feature.populate_indices
|
189
|
+
all_concept.each do |concept|
|
190
|
+
next unless concept.is_table
|
191
|
+
debug :index, "Populating indices for #{concept.name}" do
|
192
|
+
concept.populate_indices
|
195
193
|
end
|
196
194
|
end
|
197
195
|
end
|
198
196
|
debug :index, "Finished concept indices" do
|
199
|
-
|
200
|
-
next unless
|
201
|
-
next unless
|
202
|
-
|
203
|
-
|
204
|
-
feature.indices.each do |index|
|
197
|
+
all_concept.each do |concept|
|
198
|
+
next unless concept.is_table
|
199
|
+
next unless concept.indices.size > 0
|
200
|
+
debug :index, "#{concept.name}:" do
|
201
|
+
concept.indices.each do |index|
|
205
202
|
debug :index, index
|
206
203
|
end
|
207
204
|
end
|
@@ -117,7 +117,7 @@ module ActiveFacts
|
|
117
117
|
if @to && @to.fact_type
|
118
118
|
@to.name.camelwords
|
119
119
|
else
|
120
|
-
@to_role.fact_type.preferred_reading.
|
120
|
+
@to_role.fact_type.preferred_reading.text.gsub(/\{[0-9]\}/,'').strip.camelwords
|
121
121
|
end
|
122
122
|
when @to && !@to_role # @to is an objectified fact type so @to_role is a phantom
|
123
123
|
@to.name.camelwords
|
@@ -260,7 +260,8 @@ module ActiveFacts
|
|
260
260
|
|
261
261
|
when :supertype # A subtype absorbs a reference to its supertype when separate, or all when partitioned
|
262
262
|
# REVISIT: Or when partitioned
|
263
|
-
|
263
|
+
raise hell unless role.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
|
264
|
+
if role.fact_type.assimilation # assimilation == 'separate' or assimilation == 'partitioned'
|
264
265
|
debug :references, "supertype #{name} doesn't absorb a reference to separate subtype #{role.fact_type.subtype.name}"
|
265
266
|
else
|
266
267
|
r = ActiveFacts::Persistence::Reference.new(self, role)
|
@@ -270,7 +271,8 @@ module ActiveFacts
|
|
270
271
|
end
|
271
272
|
|
272
273
|
when :subtype # This object is a supertype, which can absorb the subtype unless that's independent
|
273
|
-
|
274
|
+
raise hell unless role.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
|
275
|
+
if role.fact_type.assimilation
|
274
276
|
ActiveFacts::Persistence::Reference.new(self, role).tabulate
|
275
277
|
# If partitioned, the supertype is absorbed into *each* subtype; a reference to the supertype needs to know which
|
276
278
|
else
|
@@ -282,6 +284,7 @@ module ActiveFacts
|
|
282
284
|
|
283
285
|
# Decide which way the one-to-one is likely to go; it will be flipped later if necessary.
|
284
286
|
# Force the decision if just one is independent:
|
287
|
+
# REVISIT: Decide whether supertype assimilation can affect this
|
285
288
|
r.tabulate and return if is_independent and !r.to.is_independent
|
286
289
|
return if !is_independent and r.to.is_independent
|
287
290
|
|
@@ -335,25 +338,22 @@ module ActiveFacts
|
|
335
338
|
class Vocabulary
|
336
339
|
def populate_all_references #:nodoc:
|
337
340
|
debug :references, "Populating all concept references" do
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
feature.tentative = true # Uncertain
|
341
|
+
all_concept.each do |concept|
|
342
|
+
concept.clear_references
|
343
|
+
concept.is_table = nil # Undecided; force an attempt to decide
|
344
|
+
concept.tentative = true # Uncertain
|
343
345
|
end
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
feature.populate_references
|
346
|
+
all_concept.each do |concept|
|
347
|
+
debug :references, "Populating references for #{concept.name}" do
|
348
|
+
concept.populate_references
|
348
349
|
end
|
349
350
|
end
|
350
351
|
end
|
351
352
|
debug :references, "Finished concept references" do
|
352
|
-
|
353
|
-
next unless
|
354
|
-
|
355
|
-
|
356
|
-
feature.references_from.each do |ref|
|
353
|
+
all_concept.each do |concept|
|
354
|
+
next unless concept.references_from.size > 0
|
355
|
+
debug :references, "#{concept.name}:" do
|
356
|
+
concept.references_from.each do |ref|
|
357
357
|
debug :references, "#{ref}"
|
358
358
|
end
|
359
359
|
end
|
@@ -88,8 +88,7 @@ module ActiveFacts
|
|
88
88
|
# Subtypes are not a table unless partitioned or separate
|
89
89
|
# REVISIT: Support partitioned subtypes here
|
90
90
|
if (!supertypes.empty?)
|
91
|
-
|
92
|
-
return @is_table = false
|
91
|
+
return @is_table = all_supertype_inheritance.detect{|ti| ti.assimilation} != nil
|
93
92
|
end
|
94
93
|
|
95
94
|
# If the preferred_identifier includes an auto_assigned ValueType
|
@@ -169,7 +168,7 @@ module ActiveFacts
|
|
169
168
|
# 2) Decide which Concepts must be and must not be tables
|
170
169
|
# a. Concepts labelled is_independent are tables (See the is_table methods above)
|
171
170
|
# b. Entity types having no references to them must be tables
|
172
|
-
# c. subtypes are not tables unless marked
|
171
|
+
# c. subtypes are not tables unless marked with assimilation = separate or partitioned
|
173
172
|
# d. ValueTypes are never tables unless they can have references (to other ValueTypes)
|
174
173
|
# e. An EntityType having an identifying AutoInc field must be a table unless it has exactly one reference
|
175
174
|
# f. An EntityType whose only reference is through its single preferred_identifier role gets absorbed
|
@@ -182,19 +181,18 @@ module ActiveFacts
|
|
182
181
|
populate_all_references
|
183
182
|
|
184
183
|
debug :absorption, "Calculating relational composition" do
|
185
|
-
# Evaluate the possible independence of each concept, building an array of
|
184
|
+
# Evaluate the possible independence of each concept, building an array of concepts of indeterminate status:
|
186
185
|
undecided =
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
feature.tentative # Selection criterion
|
186
|
+
all_concept.select do |concept|
|
187
|
+
concept.is_table # Ask it whether it thinks it should be a table
|
188
|
+
concept.tentative # Selection criterion
|
191
189
|
end
|
192
190
|
|
193
191
|
if debug :absorption, "Generating tables, #{undecided.size} undecided"
|
194
|
-
(
|
195
|
-
next if ValueType ===
|
192
|
+
(all_concept-undecided).each {|concept|
|
193
|
+
next if ValueType === concept && !concept.is_table # Skip unremarkable cases
|
196
194
|
debug :absorption do
|
197
|
-
debug :absorption, "#{
|
195
|
+
debug :absorption, "#{concept.name} is #{concept.is_table ? "" : "not "}a table#{concept.tentative ? ", tentatively" : ""}"
|
198
196
|
end
|
199
197
|
}
|
200
198
|
end
|
@@ -205,80 +203,81 @@ module ActiveFacts
|
|
205
203
|
debug :absorption, "Starting composition pass #{pass} with #{undecided.size} undecided tables"
|
206
204
|
possible_flips = {} # A hash by table containing an array of references that can be flipped
|
207
205
|
finalised = # Make an array of things we finalised during this pass
|
208
|
-
undecided.select do |
|
209
|
-
debug :absorption, "Considering #{
|
210
|
-
debug :absorption, "refs to #{
|
211
|
-
debug :absorption, "refs from #{
|
206
|
+
undecided.select do |concept|
|
207
|
+
debug :absorption, "Considering #{concept.name}:" do
|
208
|
+
debug :absorption, "refs to #{concept.name} are from #{concept.references_to.map{|ref| ref.from.name}*", "}" if concept.references_to.size > 0
|
209
|
+
debug :absorption, "refs from #{concept.name} are to #{concept.references_from.map{|ref| ref.to.name rescue ref.fact_type.default_reading}*", "}" if concept.references_from.size > 0
|
212
210
|
|
213
211
|
# Always absorb an objectified unary into its role player:
|
214
|
-
if
|
215
|
-
debug :absorption, "Absorb objectified unary #{
|
216
|
-
|
217
|
-
next
|
212
|
+
if concept.fact_type && concept.fact_type.all_role.size == 1
|
213
|
+
debug :absorption, "Absorb objectified unary #{concept.name} into #{concept.fact_type.entity_type.name}"
|
214
|
+
concept.definitely_not_table
|
215
|
+
next concept
|
218
216
|
end
|
219
217
|
|
220
218
|
# If the PI contains one role only, played by an entity type that can absorb us, do that.
|
221
|
-
pi_roles =
|
219
|
+
pi_roles = concept.preferred_identifier.role_sequence.all_role_ref.map(&:role)
|
222
220
|
debug :absorption, "pi_roles are played by #{pi_roles.map{|role| role.concept.name}*", "}"
|
223
221
|
first_pi_role = pi_roles[0]
|
224
222
|
pi_ref = nil
|
225
223
|
if pi_roles.size == 1 and
|
226
|
-
|
224
|
+
concept.references_to.detect{|ref| pi_ref = ref if ref.from_role == first_pi_role && ref.from.is_a?(EntityType)}
|
227
225
|
|
228
|
-
debug :absorption, "#{
|
229
|
-
|
230
|
-
next
|
226
|
+
debug :absorption, "#{concept.name} is fully absorbed along its sole reference path into entity type #{pi_ref.from.name}"
|
227
|
+
concept.definitely_not_table
|
228
|
+
next concept
|
231
229
|
end
|
232
230
|
|
233
231
|
# If there's more than one absorption path and any functional dependencies that can't absorb us, it's a table
|
234
232
|
non_identifying_refs_from =
|
235
|
-
|
233
|
+
concept.references_from.reject{|ref|
|
236
234
|
pi_roles.include?(ref.to_role)
|
237
235
|
}
|
238
|
-
debug :absorption, "#{
|
236
|
+
debug :absorption, "#{concept.name} has #{non_identifying_refs_from.size} non-identifying functional roles"
|
239
237
|
|
240
|
-
if
|
238
|
+
if concept.references_to.size > 1 and
|
241
239
|
non_identifying_refs_from.size > 0
|
242
|
-
debug :absorption, "#{
|
243
|
-
|
244
|
-
next
|
240
|
+
debug :absorption, "#{concept.name} has non-identifying functional dependencies so 3NF requires it be a table"
|
241
|
+
concept.definitely_table
|
242
|
+
next concept
|
245
243
|
end
|
246
244
|
|
247
245
|
absorption_paths =
|
248
246
|
(
|
249
247
|
non_identifying_refs_from.reject do |ref|
|
250
248
|
!ref.to or ref.to.absorbed_via == ref
|
251
|
-
end+
|
249
|
+
end+concept.references_to
|
252
250
|
).reject do |ref|
|
253
251
|
next true if !ref.to.is_table or
|
254
252
|
![:one_one, :supertype, :subtype].include?(ref.role_type)
|
255
253
|
|
256
|
-
#
|
254
|
+
# Don't absorb an object along a non-mandatory role (otherwise if it doesn't play that role, it can't exist either)
|
257
255
|
from_is_mandatory = !!ref.is_mandatory
|
258
256
|
to_is_mandatory = !ref.to_role || !!ref.to_role.is_mandatory
|
259
|
-
|
260
|
-
|
257
|
+
|
258
|
+
bad = !(ref.from == concept ? from_is_mandatory : to_is_mandatory)
|
259
|
+
debug :absorption, "Not absorbing #{concept.name} through non-mandatory #{ref}" if bad
|
261
260
|
bad
|
262
261
|
end
|
263
262
|
|
264
263
|
# If this object can be fully absorbed, do that (might require flipping some references)
|
265
264
|
if absorption_paths.size > 0
|
266
|
-
debug :absorption, "#{
|
265
|
+
debug :absorption, "#{concept.name} is fully absorbed through #{absorption_paths.inspect}"
|
267
266
|
absorption_paths.each do |ref|
|
268
|
-
debug :absorption, "flip #{ref} so #{
|
269
|
-
ref.flip if
|
267
|
+
debug :absorption, "flip #{ref} so #{concept.name} can be absorbed"
|
268
|
+
ref.flip if concept == ref.from
|
270
269
|
end
|
271
|
-
|
272
|
-
next
|
270
|
+
concept.definitely_not_table
|
271
|
+
next concept
|
273
272
|
end
|
274
273
|
|
275
274
|
if non_identifying_refs_from.size == 0
|
276
|
-
# and (!
|
275
|
+
# and (!concept.is_a?(EntityType) ||
|
277
276
|
# # REVISIT: The roles may be collectively but not individually mandatory.
|
278
|
-
#
|
279
|
-
debug :absorption, "#{
|
280
|
-
|
281
|
-
next
|
277
|
+
# concept.references_to.detect { |ref| !ref.from_role || ref.from_role.is_mandatory })
|
278
|
+
debug :absorption, "#{concept.name} is fully absorbed in #{concept.references_to.size} places: #{concept.references_to.map{|ref| ref.from.name}*", "}"
|
279
|
+
concept.definitely_not_table
|
280
|
+
next concept
|
282
281
|
end
|
283
282
|
|
284
283
|
false # Failed to decide about this entity_type this time around
|
@@ -291,18 +290,18 @@ module ActiveFacts
|
|
291
290
|
|
292
291
|
# A ValueType that isn't explicitly a table and isn't needed anywhere doesn't matter,
|
293
292
|
# unless it should absorb something else (another ValueType is all it could be):
|
294
|
-
|
295
|
-
if (!
|
296
|
-
debug :absorption, "Making #{
|
297
|
-
|
293
|
+
all_concept.each do |concept|
|
294
|
+
if (!concept.is_table and concept.references_to.size == 0 and concept.references_from.size > 0)
|
295
|
+
debug :absorption, "Making #{concept.name} a table; it has nowhere else to go and needs to absorb things"
|
296
|
+
concept.probably_table
|
298
297
|
end
|
299
298
|
end
|
300
299
|
|
301
300
|
# Now, evaluate all possibilities of the tentative assignments
|
302
301
|
# Incomplete. Apparently unnecessary as well... so far. We'll see.
|
303
302
|
if debug :absorption
|
304
|
-
undecided.each do |
|
305
|
-
debug :absorption, "Unable to decide independence of #{
|
303
|
+
undecided.each do |concept|
|
304
|
+
debug :absorption, "Unable to decide independence of #{concept.name}, going with #{concept.show_tabular}"
|
306
305
|
end
|
307
306
|
end
|
308
307
|
end
|
@@ -311,7 +310,7 @@ module ActiveFacts
|
|
311
310
|
populate_all_indices
|
312
311
|
|
313
312
|
@tables =
|
314
|
-
|
313
|
+
all_concept.
|
315
314
|
select { |f| f.is_table }.
|
316
315
|
sort_by { |table| table.name }
|
317
316
|
end
|
data/lib/activefacts/version.rb
CHANGED
@@ -13,7 +13,9 @@ module ActiveFacts
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def preferred_reading
|
16
|
-
all_reading_by_ordinal[0]
|
16
|
+
p = all_reading_by_ordinal[0]
|
17
|
+
raise "No reading for (#{all_role.map{|r| r.concept.name}*", "})" unless p
|
18
|
+
p
|
17
19
|
end
|
18
20
|
|
19
21
|
def describe(highlight = nil)
|
@@ -39,7 +41,7 @@ module ActiveFacts
|
|
39
41
|
rs.all_presence_constraint.detect{|pc|
|
40
42
|
pc.max_frequency == 1
|
41
43
|
}
|
42
|
-
}
|
44
|
+
} ? true : false
|
43
45
|
end
|
44
46
|
|
45
47
|
def is_mandatory
|
@@ -49,7 +51,7 @@ module ActiveFacts
|
|
49
51
|
rs.all_presence_constraint.detect{|pc|
|
50
52
|
pc.min_frequency and pc.min_frequency >= 1 and pc.is_mandatory
|
51
53
|
}
|
52
|
-
}
|
54
|
+
} ? true : false
|
53
55
|
end
|
54
56
|
|
55
57
|
# Return the RoleRef to this role from its fact type's preferred_reading
|
@@ -79,7 +81,7 @@ module ActiveFacts
|
|
79
81
|
def role_name(joiner = "-")
|
80
82
|
name_array =
|
81
83
|
if role.fact_type.all_role.size == 1
|
82
|
-
role.fact_type.preferred_reading.
|
84
|
+
role.fact_type.preferred_reading.text.gsub(/\{[0-9]\}/,'').strip.split(/\s/)
|
83
85
|
else
|
84
86
|
role.role_name || [leading_adjective, role.concept.name, trailing_adjective].compact.map{|w| w.split(/\s/)}.flatten
|
85
87
|
end
|
@@ -310,8 +312,8 @@ module ActiveFacts
|
|
310
312
|
# REVISIT: This should probably be changed to be the fact role sequence.
|
311
313
|
#
|
312
314
|
# define_role_names here is false (use defined names), true (define names) or nil (neither)
|
313
|
-
def expand(frequency_constraints = [], define_role_names = false)
|
314
|
-
expanded = "#{
|
315
|
+
def expand(frequency_constraints = [], define_role_names = false, literals = [])
|
316
|
+
expanded = "#{text}"
|
315
317
|
role_refs = role_sequence.all_role_ref.sort_by{|role_ref| role_ref.ordinal}
|
316
318
|
(0...role_refs.size).each{|i|
|
317
319
|
role_ref = role_refs[i]
|
@@ -337,12 +339,15 @@ module ActiveFacts
|
|
337
339
|
else
|
338
340
|
player_name = player.name
|
339
341
|
end
|
342
|
+
literal = literals[i]
|
340
343
|
[
|
341
344
|
fc ? fc : nil,
|
342
345
|
la,
|
343
346
|
define_role_names == false && role_name ? role_name : player_name,
|
344
347
|
ta,
|
345
|
-
define_role_names && role_name && player.name != role_name ? "(as #{role_name})" : nil
|
348
|
+
define_role_names && role_name && player.name != role_name ? "(as #{role_name})" : nil,
|
349
|
+
# Can't have both a literal and a restriction, but we don't enforce that here:
|
350
|
+
literal ? literal : nil
|
346
351
|
].compact*" "
|
347
352
|
}
|
348
353
|
}
|
@@ -352,7 +357,7 @@ module ActiveFacts
|
|
352
357
|
end
|
353
358
|
|
354
359
|
def words_and_role_refs
|
355
|
-
|
360
|
+
text.
|
356
361
|
scan(/(?: |\{[0-9]+\}|[^{} ]+)/). # split up the text into words
|
357
362
|
reject{|s| s==' '}. # Remove white space
|
358
363
|
map do |frag| # and go through the bits
|
@@ -365,6 +370,72 @@ module ActiveFacts
|
|
365
370
|
end
|
366
371
|
end
|
367
372
|
|
373
|
+
class ValueRestriction
|
374
|
+
def describe
|
375
|
+
"restricted to {"+
|
376
|
+
all_allowed_range_sorted.map{|ar| ar.to_s(false) }*", "+
|
377
|
+
"}"
|
378
|
+
end
|
379
|
+
|
380
|
+
def all_allowed_range_sorted
|
381
|
+
all_allowed_range.sort_by{|ar|
|
382
|
+
((min = ar.value_range.minimum_bound) && min.value.literal) ||
|
383
|
+
((max = ar.value_range.maximum_bound) && max.value.literal)
|
384
|
+
}
|
385
|
+
end
|
386
|
+
|
387
|
+
def to_s
|
388
|
+
if all_allowed_range.size > 1
|
389
|
+
"[" +
|
390
|
+
all_allowed_range.sorted.map { |ar| ar.to_s(true) }*", " +
|
391
|
+
"]"
|
392
|
+
else
|
393
|
+
all_allowed_range.single.to_s
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
class AllowedRange
|
399
|
+
def to_s(infinity = true)
|
400
|
+
min = value_range.minimum_bound
|
401
|
+
max = value_range.maximum_bound
|
402
|
+
# Open-ended string ranges will fail in Ruby
|
403
|
+
|
404
|
+
if min = value_range.minimum_bound
|
405
|
+
min = min.value
|
406
|
+
if min.is_a_string
|
407
|
+
min_literal = min.literal.inspect.gsub(/\A"|"\Z/,"'") # Escape string characters
|
408
|
+
else
|
409
|
+
min_literal = min.literal
|
410
|
+
end
|
411
|
+
else
|
412
|
+
min_literal = infinity ? "INFINITY" : ""
|
413
|
+
end
|
414
|
+
if max = value_range.maximum_bound
|
415
|
+
max = max.value
|
416
|
+
if max.is_a_string
|
417
|
+
max_literal = max.literal.inspect.gsub(/\A"|"\Z/,"'") # Escape string characters
|
418
|
+
else
|
419
|
+
max_literal = max.literal
|
420
|
+
end
|
421
|
+
else
|
422
|
+
max_literal = infinity ? "INFINITY" : ""
|
423
|
+
end
|
424
|
+
|
425
|
+
min_literal +
|
426
|
+
(min_literal != (max&&max_literal) ? (".." + max_literal) : "")
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
class Value
|
431
|
+
def to_s
|
432
|
+
(is_a_string ? literal.inspect.gsub(/\A"|"\Z/,"'") : literal) + (unit ? " " + unit.name : "")
|
433
|
+
end
|
434
|
+
def inspect
|
435
|
+
to_s
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
368
439
|
class PresenceConstraint
|
369
440
|
def frequency
|
370
441
|
min = min_frequency
|