activefacts 0.7.3 → 0.8.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. data/LICENSE +19 -0
  2. data/Manifest.txt +24 -2
  3. data/Rakefile +25 -3
  4. data/bin/afgen +1 -1
  5. data/bin/cql +13 -2
  6. data/css/offline.css +3 -0
  7. data/css/orm2.css +24 -0
  8. data/css/print.css +8 -0
  9. data/css/style-print.css +357 -0
  10. data/css/style.css +387 -0
  11. data/download.html +85 -0
  12. data/examples/CQL/Address.cql +3 -3
  13. data/examples/CQL/Blog.cql +13 -14
  14. data/examples/CQL/CompanyDirectorEmployee.cql +4 -4
  15. data/examples/CQL/Death.cql +3 -2
  16. data/examples/CQL/Genealogy.cql +13 -11
  17. data/examples/CQL/Marriage.cql +2 -2
  18. data/examples/CQL/Metamodel.cql +136 -93
  19. data/examples/CQL/MultiInheritance.cql +2 -2
  20. data/examples/CQL/OilSupply.cql +14 -10
  21. data/examples/CQL/Orienteering.cql +22 -19
  22. data/examples/CQL/PersonPlaysGame.cql +3 -2
  23. data/examples/CQL/SchoolActivities.cql +4 -2
  24. data/examples/CQL/SimplestUnary.cql +1 -1
  25. data/examples/CQL/SubtypePI.cql +6 -7
  26. data/examples/CQL/Warehousing.cql +16 -19
  27. data/examples/CQL/unit.cql +584 -0
  28. data/examples/index.html +276 -0
  29. data/examples/intro.html +497 -0
  30. data/examples/local.css +20 -0
  31. data/index.html +96 -0
  32. data/lib/activefacts/api/concept.rb +48 -46
  33. data/lib/activefacts/api/constellation.rb +43 -23
  34. data/lib/activefacts/api/entity.rb +2 -2
  35. data/lib/activefacts/api/instance.rb +6 -2
  36. data/lib/activefacts/api/instance_index.rb +5 -0
  37. data/lib/activefacts/api/value.rb +8 -2
  38. data/lib/activefacts/api/vocabulary.rb +15 -10
  39. data/lib/activefacts/cql/CQLParser.treetop +109 -88
  40. data/lib/activefacts/cql/Concepts.treetop +32 -10
  41. data/lib/activefacts/cql/Context.treetop +34 -0
  42. data/lib/activefacts/cql/Expressions.treetop +9 -9
  43. data/lib/activefacts/cql/FactTypes.treetop +30 -31
  44. data/lib/activefacts/cql/Language/English.treetop +50 -0
  45. data/lib/activefacts/cql/LexicalRules.treetop +2 -1
  46. data/lib/activefacts/cql/Terms.treetop +117 -0
  47. data/lib/activefacts/cql/ValueTypes.treetop +152 -0
  48. data/lib/activefacts/cql/compiler.rb +1718 -0
  49. data/lib/activefacts/cql/parser.rb +124 -57
  50. data/lib/activefacts/generate/absorption.rb +1 -1
  51. data/lib/activefacts/generate/cql.rb +111 -100
  52. data/lib/activefacts/generate/cql/html.rb +5 -5
  53. data/lib/activefacts/generate/oo.rb +3 -3
  54. data/lib/activefacts/generate/ordered.rb +51 -19
  55. data/lib/activefacts/generate/ruby.rb +10 -8
  56. data/lib/activefacts/generate/sql/mysql.rb +14 -10
  57. data/lib/activefacts/generate/sql/server.rb +29 -24
  58. data/lib/activefacts/input/cql.rb +9 -1264
  59. data/lib/activefacts/input/orm.rb +213 -200
  60. data/lib/activefacts/persistence/columns.rb +11 -10
  61. data/lib/activefacts/persistence/index.rb +15 -18
  62. data/lib/activefacts/persistence/reference.rb +17 -17
  63. data/lib/activefacts/persistence/tables.rb +50 -51
  64. data/lib/activefacts/version.rb +1 -1
  65. data/lib/activefacts/vocabulary/extensions.rb +79 -8
  66. data/lib/activefacts/vocabulary/metamodel.rb +183 -114
  67. data/spec/absorption_ruby_spec.rb +99 -0
  68. data/spec/absorption_spec.rb +3 -4
  69. data/spec/api/constellation.rb +1 -1
  70. data/spec/api/entity_type.rb +3 -1
  71. data/spec/api/instance.rb +4 -2
  72. data/spec/api/roles.rb +8 -6
  73. data/spec/api_spec.rb +1 -2
  74. data/spec/cql/context_spec.rb +71 -0
  75. data/spec/cql/samples_spec.rb +154 -0
  76. data/spec/cql/unit_spec.rb +375 -0
  77. data/spec/cql_cql_spec.rb +31 -21
  78. data/spec/cql_mysql_spec.rb +70 -0
  79. data/spec/cql_parse_spec.rb +15 -9
  80. data/spec/cql_ruby_spec.rb +27 -13
  81. data/spec/cql_sql_spec.rb +42 -16
  82. data/spec/cql_symbol_tables_spec.rb +2 -3
  83. data/spec/cqldump_spec.rb +7 -7
  84. data/spec/helpers/file_matcher.rb +39 -0
  85. data/spec/norma_cql_spec.rb +20 -12
  86. data/spec/norma_ruby_spec.rb +6 -3
  87. data/spec/norma_sql_spec.rb +6 -3
  88. data/spec/norma_tables_spec.rb +6 -4
  89. data/spec/spec_helper.rb +27 -8
  90. data/status.html +69 -0
  91. data/why.html +60 -0
  92. metadata +34 -11
  93. data/lib/activefacts/cql/DataTypes.treetop +0 -81
  94. 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
- all_feature.each do |feature|
362
- feature.self_value_reference if feature.is_a?(ActiveFacts::Metamodel::ValueType) && feature.is_table
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
- all_feature.each do |feature|
372
- next if !feature.is_a?(ActiveFacts::Metamodel::Concept) || !feature.is_table
373
- debug :columns, "Populating columns for table #{feature.name}" do
374
- feature.populate_columns
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
- all_feature.each do |feature|
380
- next if !feature.is_a?(ActiveFacts::Metamodel::Concept) || !feature.is_table
381
- debug :columns, "Finished columns for table #{feature.name}" do
382
- feature.columns.each do |column|
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
- all_feature.each do |feature|
187
- next unless feature.is_a? Concept
188
- feature.clear_indices
186
+ all_concept.each do |concept|
187
+ concept.clear_indices
189
188
  end
190
- all_feature.each do |feature|
191
- next unless feature.is_a? Concept
192
- next unless feature.is_table
193
- debug :index, "Populating indices for #{feature.name}" do
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
- all_feature.each do |feature|
200
- next unless feature.is_a? Concept
201
- next unless feature.is_table
202
- next unless feature.indices.size > 0
203
- debug :index, "#{feature.name}:" do
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.reading_text.gsub(/\{[0-9]\}/,'').strip.camelwords
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
- if role.fact_type.subtype.is_independent
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
- if is_independent # REVISIT: Or when partitioned
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
- all_feature.each do |feature|
339
- next unless feature.is_a? Concept
340
- feature.clear_references
341
- feature.is_table = nil # Undecided; force an attempt to decide
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
- all_feature.each do |feature|
345
- next unless feature.is_a? Concept
346
- debug :references, "Populating references for #{feature.name}" do
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
- all_feature.each do |feature|
353
- next unless feature.is_a? Concept
354
- next unless feature.references_from.size > 0
355
- debug :references, "#{feature.name}:" do
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
- av = all_supertype_inheritance[0]
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 is_independent (separate) or partitioned (not yet impl)
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 features of indeterminate status:
184
+ # Evaluate the possible independence of each concept, building an array of concepts of indeterminate status:
186
185
  undecided =
187
- all_feature.select do |feature|
188
- next unless feature.is_a? Concept
189
- feature.is_table # Ask it whether it thinks it should be a table
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
- (all_feature-undecided).each {|feature|
195
- next if ValueType === feature && !feature.is_table # Skip unremarkable cases
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, "#{feature.name} is #{feature.is_table ? "" : "not "}a table#{feature.tentative ? ", tentatively" : ""}"
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 |feature|
209
- debug :absorption, "Considering #{feature.name}:" do
210
- debug :absorption, "refs to #{feature.name} are from #{feature.references_to.map{|ref| ref.from.name}*", "}" if feature.references_to.size > 0
211
- debug :absorption, "refs from #{feature.name} are to #{feature.references_from.map{|ref| ref.to.name rescue ref.fact_type.default_reading}*", "}" if feature.references_from.size > 0
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 feature.fact_type && feature.fact_type.all_role.size == 1
215
- debug :absorption, "Absorb objectified unary #{feature.name} into #{feature.fact_type.entity_type.name}"
216
- feature.definitely_not_table
217
- next feature
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 = feature.preferred_identifier.role_sequence.all_role_ref.map(&:role)
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
- feature.references_to.detect{|ref| pi_ref = ref if ref.from_role == first_pi_role && ref.from.is_a?(EntityType)}
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, "#{feature.name} is fully absorbed along its sole reference path into entity type #{pi_ref.from.name}"
229
- feature.definitely_not_table
230
- next feature
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
- feature.references_from.reject{|ref|
233
+ concept.references_from.reject{|ref|
236
234
  pi_roles.include?(ref.to_role)
237
235
  }
238
- debug :absorption, "#{feature.name} has #{non_identifying_refs_from.size} non-identifying functional roles"
236
+ debug :absorption, "#{concept.name} has #{non_identifying_refs_from.size} non-identifying functional roles"
239
237
 
240
- if feature.references_to.size > 1 and
238
+ if concept.references_to.size > 1 and
241
239
  non_identifying_refs_from.size > 0
242
- debug :absorption, "#{feature.name} has non-identifying functional dependencies so 3NF requires it be a table"
243
- feature.definitely_table
244
- next feature
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+feature.references_to
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
- # If one side is mandatory but not the other, don't absorb the mandatory side into the non-mandatory one
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
- bad = (to_is_mandatory != from_is_mandatory and (ref.from == feature ? from_is_mandatory : to_is_mandatory))
260
- debug :absorption, "Not absorbing mandatory #{feature.name} through #{ref}" if bad
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, "#{feature.name} is fully absorbed through #{absorption_paths.inspect}"
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 #{feature.name} can be absorbed"
269
- ref.flip if feature == ref.from
267
+ debug :absorption, "flip #{ref} so #{concept.name} can be absorbed"
268
+ ref.flip if concept == ref.from
270
269
  end
271
- feature.definitely_not_table
272
- next feature
270
+ concept.definitely_not_table
271
+ next concept
273
272
  end
274
273
 
275
274
  if non_identifying_refs_from.size == 0
276
- # and (!feature.is_a?(EntityType) ||
275
+ # and (!concept.is_a?(EntityType) ||
277
276
  # # REVISIT: The roles may be collectively but not individually mandatory.
278
- # feature.references_to.detect { |ref| !ref.from_role || ref.from_role.is_mandatory })
279
- debug :absorption, "#{feature.name} is fully absorbed in #{feature.references_to.size} places: #{feature.references_to.map{|ref| ref.from.name}*", "}"
280
- feature.definitely_not_table
281
- next feature
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
- all_feature.each do |feature|
295
- if (!feature.is_table and feature.references_to.size == 0 and feature.references_from.size > 0)
296
- debug :absorption, "Making #{feature.name} a table; it has nowhere else to go and needs to absorb things"
297
- feature.probably_table
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 |feature|
305
- debug :absorption, "Unable to decide independence of #{feature.name}, going with #{feature.show_tabular}"
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
- all_feature.
313
+ all_concept.
315
314
  select { |f| f.is_table }.
316
315
  sort_by { |table| table.name }
317
316
  end
@@ -5,5 +5,5 @@
5
5
  # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
6
  #
7
7
  module ActiveFacts
8
- VERSION = '0.7.3'
8
+ VERSION = '0.8.5'
9
9
  end
@@ -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.reading_text.gsub(/\{[0-9]\}/,'').strip.split(/\s/)
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 = "#{reading_text}"
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
- reading_text.
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