activefacts 0.7.0 → 0.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.
Files changed (60) hide show
  1. data/README.rdoc +0 -3
  2. data/Rakefile +7 -5
  3. data/bin/afgen +5 -2
  4. data/bin/cql +3 -2
  5. data/examples/CQL/Genealogy.cql +3 -3
  6. data/examples/CQL/Metamodel.cql +10 -7
  7. data/examples/CQL/MultiInheritance.cql +2 -1
  8. data/examples/CQL/OilSupply.cql +4 -4
  9. data/examples/CQL/Orienteering.cql +2 -2
  10. data/lib/activefacts.rb +2 -1
  11. data/lib/activefacts/api.rb +21 -2
  12. data/lib/activefacts/api/concept.rb +52 -39
  13. data/lib/activefacts/api/constellation.rb +8 -6
  14. data/lib/activefacts/api/entity.rb +41 -37
  15. data/lib/activefacts/api/instance.rb +5 -3
  16. data/lib/activefacts/api/numeric.rb +28 -21
  17. data/lib/activefacts/api/role.rb +29 -43
  18. data/lib/activefacts/api/standard_types.rb +8 -3
  19. data/lib/activefacts/api/support.rb +4 -4
  20. data/lib/activefacts/api/value.rb +9 -3
  21. data/lib/activefacts/api/vocabulary.rb +17 -7
  22. data/lib/activefacts/cql.rb +10 -7
  23. data/lib/activefacts/cql/CQLParser.treetop +6 -0
  24. data/lib/activefacts/cql/Concepts.treetop +32 -26
  25. data/lib/activefacts/cql/DataTypes.treetop +6 -0
  26. data/lib/activefacts/cql/Expressions.treetop +6 -0
  27. data/lib/activefacts/cql/FactTypes.treetop +6 -0
  28. data/lib/activefacts/cql/Language/English.treetop +9 -3
  29. data/lib/activefacts/cql/LexicalRules.treetop +6 -0
  30. data/lib/activefacts/cql/Rakefile +8 -0
  31. data/lib/activefacts/cql/parser.rb +4 -2
  32. data/lib/activefacts/generate/absorption.rb +20 -28
  33. data/lib/activefacts/generate/cql.rb +28 -16
  34. data/lib/activefacts/generate/cql/html.rb +327 -321
  35. data/lib/activefacts/generate/null.rb +7 -3
  36. data/lib/activefacts/generate/oo.rb +19 -15
  37. data/lib/activefacts/generate/ordered.rb +457 -461
  38. data/lib/activefacts/generate/ruby.rb +12 -4
  39. data/lib/activefacts/generate/sql/server.rb +42 -10
  40. data/lib/activefacts/generate/text.rb +7 -3
  41. data/lib/activefacts/input/cql.rb +55 -28
  42. data/lib/activefacts/input/orm.rb +32 -22
  43. data/lib/activefacts/persistence.rb +5 -0
  44. data/lib/activefacts/persistence/columns.rb +66 -32
  45. data/lib/activefacts/persistence/foreignkey.rb +29 -5
  46. data/lib/activefacts/persistence/index.rb +57 -25
  47. data/lib/activefacts/persistence/reference.rb +65 -30
  48. data/lib/activefacts/persistence/tables.rb +28 -17
  49. data/lib/activefacts/support.rb +8 -0
  50. data/lib/activefacts/version.rb +7 -1
  51. data/lib/activefacts/vocabulary.rb +4 -2
  52. data/lib/activefacts/vocabulary/extensions.rb +12 -10
  53. data/lib/activefacts/vocabulary/metamodel.rb +24 -23
  54. data/spec/api/autocounter.rb +2 -2
  55. data/spec/api/entity_type.rb +2 -2
  56. data/spec/api/instance.rb +61 -30
  57. data/spec/api/roles.rb +9 -9
  58. data/spec/cql_parse_spec.rb +1 -0
  59. data/spec/norma_tables_spec.rb +3 -3
  60. metadata +8 -4
@@ -1,14 +1,22 @@
1
1
  #
2
- # Generate Ruby for the ActiveFacts API from an ActiveFacts vocabulary.
3
- # Copyright (c) 2008 Clifford Heath. Read the LICENSE file.
2
+ # ActiveFacts Generators.
3
+ # Generate Ruby classes for the ActiveFacts API from an ActiveFacts vocabulary.
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
4
6
  #
5
7
  require 'activefacts/vocabulary'
6
8
  require 'activefacts/generate/oo'
7
9
 
8
10
  module ActiveFacts
9
-
10
11
  module Generate
12
+ # Generate Ruby module containing classes for an ActiveFacts vocabulary.
13
+ # Invoke as
14
+ # afgen --ruby[=options] <file>.cql
15
+ # Options are comma or space separated:
16
+ # * help list available options
17
+ # * sql Emit the sql mapping for tables/columns (REVISIT: not functional at present)
11
18
  class RUBY < OO
19
+ private
12
20
 
13
21
  def set_option(option)
14
22
  @sql ||= false
@@ -131,7 +139,7 @@ module ActiveFacts
131
139
  end
132
140
 
133
141
  def role_dump(role)
134
- other_role = role.fact_type.all_role[role.fact_type.all_role[0] != role ? 0 : -1]
142
+ other_role = role.fact_type.all_role.select{|r| r != role}[0] || role
135
143
  if @ar_by_role and @ar_by_role[other_role] and @sql
136
144
  puts " # role #{role.fact_type.describe(role)}: absorbs in through #{preferred_role_name(other_role)}: "+@ar_by_role[other_role].map(&:column_name)*", "
137
145
  end
@@ -1,15 +1,25 @@
1
1
  #
2
- # Generate an SQL Server schema from an ActiveFacts vocabulary.
3
- # Copyright (c) 2008 Clifford Heath. Read the LICENSE file.
2
+ # ActiveFacts Generators.
3
+ # Generate SQL for SQL Server from an ActiveFacts vocabulary.
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
4
6
  #
5
7
  require 'activefacts/vocabulary'
6
8
  require 'activefacts/persistence'
7
9
 
8
10
  module ActiveFacts
9
11
  module Generate
10
- class SQL
12
+ class SQL #:nodoc:
13
+ # Generate SQL for SQL Server for an ActiveFacts vocabulary.
14
+ # Invoke as
15
+ # afgen --sql/server[=options] <file>.cql
16
+ # Options are comma or space separated:
17
+ # * delay_fks Leave all foreign keys until the end, not just those that contain forward-references
18
+ # * norma Translate datatypes from NORMA to SQL Server
11
19
  class SERVER
20
+ private
12
21
  include Metamodel
22
+ include Persistence
13
23
  ColumnNameMax = 40
14
24
 
15
25
  RESERVED_WORDS = %w{
@@ -98,7 +108,8 @@ module ActiveFacts
98
108
  [sql_type, length]
99
109
  end
100
110
 
101
- def generate(out = $>)
111
+ public
112
+ def generate(out = $>) #:nodoc:
102
113
  @out = out
103
114
  #go "CREATE SCHEMA #{@vocabulary.name}"
104
115
 
@@ -116,7 +127,9 @@ module ActiveFacts
116
127
  column.references[0].is_simple_reference
117
128
  end
118
129
 
119
- columns = table.columns.map do |column|
130
+ columns = table.columns.sort_by do |column|
131
+ column.name(nil)
132
+ end.map do |column|
120
133
  name = escape column.name("")
121
134
  padding = " "*(name.size >= ColumnNameMax ? 1 : ColumnNameMax-name.size)
122
135
  type, params, restrictions = column.type
@@ -145,11 +158,22 @@ module ActiveFacts
145
158
  ")"
146
159
 
147
160
  inline_fks = []
148
- table.foreign_keys.each do |fk|
161
+ table.foreign_keys.sort_by do |fk|
162
+ # Put the Foreign keys in a defined order:
163
+ [ fk.to.name,
164
+ fk.to_columns.map{|col| col.name(nil).sort},
165
+ fk.from_columns.map{|col| col.name(nil).sort}
166
+ ]
167
+ end.each do |fk|
168
+ # Put the column pairs in a defined order, sorting key pairs by to-name:
169
+ froms, tos = fk.from_columns.zip(fk.to_columns).sort_by { |pair|
170
+ pair[1].name(nil)
171
+ }.transpose
172
+
149
173
  fk_text = "FOREIGN KEY (" +
150
- fk.from_columns.map{|column| column.name}*", " +
174
+ froms.map{|column| column.name}*", " +
151
175
  ") REFERENCES #{escape fk.to.name} (" +
152
- fk.to_columns.map{|column| column.name}*", " +
176
+ tos.map{|column| column.name}*", " +
153
177
  ")"
154
178
  if !@delay_fks and # We don't want to delay all Fks
155
179
  (tables_emitted[fk.to] or # The target table has been emitted
@@ -163,7 +187,10 @@ module ActiveFacts
163
187
  indices = table.indices
164
188
  inline_indices = []
165
189
  delayed_indices = []
166
- indices.each do |index|
190
+ indices.sort_by do |index|
191
+ # Put the indices in a defined order:
192
+ index.columns.map(&:name)
193
+ end.each do |index|
167
194
  next if index.over == table && index.is_primary # Already did the primary keys
168
195
  abbreviated_column_names = index.abbreviated_column_names*""
169
196
  column_names = index.column_names
@@ -203,11 +230,16 @@ CREATE UNIQUE CLUSTERED INDEX #{escape index.name} ON dbo.#{view_name}(#{index.c
203
230
  end
204
231
  end
205
232
 
233
+ private
206
234
  def check_clause(column_name, restrictions)
207
235
  return "" if restrictions.empty?
208
236
  # REVISIT: Merge all restrictions (later; now just use the first)
209
237
  " CHECK(" +
210
- restrictions[0].all_allowed_range.map do |ar|
238
+ restrictions[0].all_allowed_range.sort_by do |ar|
239
+ # Put the allowed ranges into a defined order:
240
+ ((min = ar.value_range.minimum_bound) && min.value) ||
241
+ ((max = ar.value_range.maximum_bound) && max.value)
242
+ end.map do |ar|
211
243
  vr = ar.value_range
212
244
  min = vr.minimum_bound
213
245
  max = vr.maximum_bound
@@ -1,12 +1,16 @@
1
1
  #
2
- # Generate text output for ActiveFacts vocabularies.
2
+ # ActiveFacts Generators.
3
+ # Generate text output (verbalise the meta-vocabulary) for ActiveFacts vocabularies.
3
4
  #
4
- # Copyright (c) 2007 Clifford Heath. Read the LICENSE file.
5
- # Author: Clifford Heath.
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
6
  #
7
7
  module ActiveFacts
8
8
  module Generate
9
+ # Generate a text verbalisation of the metamodel constellation created for an ActiveFacts vocabulary.
10
+ # Invoke as
11
+ # afgen --text <file>.cql
9
12
  class TEXT
13
+ private
10
14
  def initialize(vocabulary)
11
15
  @vocabulary = vocabulary
12
16
  @vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
@@ -1,6 +1,8 @@
1
1
  #
2
- # Compile a CQL file into an ActiveFacts vocabulary.
3
- # Copyright (c) 2008 Clifford Heath. Read the LICENSE file.
2
+ # ActiveFacts Vocabulary Input.
3
+ # Compile a CQL file into an ActiveFacts vocabulary.
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
4
6
  #
5
7
  require 'activefacts/vocabulary'
6
8
  require 'activefacts/cql/parser'
@@ -9,10 +11,11 @@ require 'ruby-debug'
9
11
 
10
12
  module ActiveFacts
11
13
  module Input #:nodoc:
12
- # The CQL Input module is activated whenever afgen is called upon to process a file
13
- # whose name ends in .cql. The file is parsed to a constellation and the vocabulary
14
- # object defined in that file is returned.
14
+ # Compile CQL to an ActiveFacts vocabulary.
15
+ # Invoke as
16
+ # afgen --<generator> <file>.cql
15
17
  class CQL
18
+ private
16
19
  include ActiveFacts
17
20
  include ActiveFacts::Metamodel
18
21
 
@@ -44,8 +47,9 @@ module ActiveFacts
44
47
  @filename = filename
45
48
  end
46
49
 
50
+ public
47
51
  # Read the input, returning a new Vocabulary:
48
- def read
52
+ def read #:nodoc:
49
53
  @constellation = ActiveFacts::API::Constellation.new(ActiveFacts::Metamodel)
50
54
 
51
55
  @parser = ActiveFacts::CQLParser.new
@@ -86,6 +90,7 @@ module ActiveFacts
86
90
  @vocabulary
87
91
  end
88
92
 
93
+ private
89
94
  def value_type(name, base_type_name, parameters, unit, ranges)
90
95
  length, scale = *parameters
91
96
 
@@ -167,7 +172,7 @@ module ActiveFacts
167
172
  clauses_by_fact_type(clauses).each do |clauses_for_fact_type|
168
173
  fact_type = nil
169
174
  @symbols.embedded_presence_constraints = [] # Clear embedded_presence_constraints for each fact type
170
- debug "New Fact Type for entity #{name}" do
175
+ debug :entity, "New Fact Type for entity #{name}" do
171
176
  clauses_for_fact_type.each do |clause|
172
177
  type, qualifiers, reading = *clause
173
178
  debug :reading, "Clause: #{clause.inspect}" do
@@ -179,6 +184,7 @@ module ActiveFacts
179
184
  end
180
185
 
181
186
  # Find the role that this entity type plays in the fact type, if any:
187
+ debug :reading, "Roles are: #{fact_type.all_role.map{|role| (role.concept == entity_type ? "*" : "") + role.concept.name }*", "}"
182
188
  player_roles = fact_type.all_role.select{|role| role.concept == entity_type }
183
189
  raise "#{role.concept.name} may only play one role in each identifying fact type" if player_roles.size > 1
184
190
  if player_role = player_roles[0]
@@ -187,11 +193,14 @@ module ActiveFacts
187
193
  raise "#{name} cannot be identified by a role in a non-binary fact type" if non_player_roles.size > 1
188
194
  elsif identification
189
195
  # This situation occurs when an objectified fact type has an entity identifier
190
- raise "Entity type #{name} may only objectify a single fact type" if entity_type.fact_type
196
+ #raise "Entity type #{name} cannot objectify fact type #{fact_type.describe}, it already objectifies #{entity_type.fact_type.describe}" if entity_type.fact_type
197
+ raise "Entity type #{name} cannot objectify fact type #{identification.inspect}, it already objectifies #{entity_type.fact_type.describe}" if entity_type.fact_type
198
+ debug :entity, "Entity type #{name} objectifies fact type #{fact_type.describe} with distinct identifier"
191
199
 
192
200
  entity_type.fact_type = fact_type
193
201
  fact_type_identification(fact_type, name, false)
194
202
  else
203
+ debug :entity, "Entity type #{name} objectifies fact type #{fact_type.describe}"
195
204
  # it's an objectified fact type, such as a subtype
196
205
  entity_type.fact_type = fact_type
197
206
  end
@@ -255,13 +264,9 @@ module ActiveFacts
255
264
 
256
265
  debug :mode, "Processing Reference Mode for #{name}#{mode_fact_type ? " with existing '#{mode_fact_type.default_reading}'" : ""}"
257
266
 
258
- # WARNING: Several things here depend on the method and order of creation of roles and role sequences above
259
- # But heck, that's what tests are for, right?
260
-
261
267
  # Fact Type:
262
268
  if (ft = mode_fact_type)
263
- entity_role = ft.all_role[n = (ft.all_role[0].concept == entity_type ? 0 : 1)]
264
- value_role = ft.all_role[1-n]
269
+ entity_role, value_role = ft.all_role.partition{|role| role.concept == entity_type}.flatten
265
270
  else
266
271
  ft = @constellation.FactType(:new)
267
272
  entity_role = @constellation.Role(ft, 0, entity_type)
@@ -271,23 +276,28 @@ module ActiveFacts
271
276
 
272
277
  # Forward reading, if it doesn't already exist:
273
278
  rss = entity_role.all_role_ref.map{|rr| rr.role_sequence.all_role_ref.size == 2 ? rr.role_sequence : nil }.compact
274
- rs01 = rss.select{|rs| rs.all_role_ref[1].role == value_role }[0]
275
- rs10 = rss.select{|rs| rs.all_role_ref[1].role == entity_role }[0]
279
+ # Find or create RoleSequences for the forward and reverse readings:
280
+ rs01 = rss.select{|rs| rs.all_role_ref.sort_by{|rr| rr.ordinal}.map(&:role) == [entity_role, value_role] }[0]
276
281
  if !rs01
277
282
  rs01 = @constellation.RoleSequence(:new)
278
283
  @constellation.RoleRef(rs01, 0, :role => entity_role)
279
284
  @constellation.RoleRef(rs01, 1, :role => value_role)
285
+ end
286
+ if rs01.all_reading.empty?
280
287
  @constellation.Reading(ft, ft.all_reading.size, :role_sequence => rs01, :reading_text => "{0} has {1}")
281
288
  debug :mode, "Creating new forward reading '#{name} has #{vt.name}'"
282
289
  else
283
- debug :mode, "Using existing forward reading #{rs01.all_reading[0].expand}"
290
+ debug :mode, "Using existing forward reading"
284
291
  end
285
292
 
286
293
  # Reverse reading:
294
+ rs10 = rss.select{|rs| rs.all_role_ref.sort_by{|rr| rr.ordinal}.map(&:role) == [value_role, entity_role] }[0]
287
295
  if !rs10
288
296
  rs10 = @constellation.RoleSequence(:new)
289
297
  @constellation.RoleRef(rs10, 0, :role => value_role)
290
298
  @constellation.RoleRef(rs10, 1, :role => entity_role)
299
+ end
300
+ if rs10.all_reading.empty?
291
301
  @constellation.Reading(ft, ft.all_reading.size, :role_sequence => rs10, :reading_text => "{0} is of {1}")
292
302
  debug :mode, "Creating new reverse reading '#{vt.name} is of #{name}'"
293
303
  else
@@ -552,6 +562,9 @@ module ActiveFacts
552
562
 
553
563
  # Create the role sequences and their role references.
554
564
  # Each role sequence contain one RoleRef for each common binding
565
+ # REVISIT: This results in ordering all RoleRefs according to the order of the common_bindings.
566
+ # This for example means that a set constraint having joins might have the join order changed so they all match.
567
+ # When you create e.g. a subset constraint in NORMA, make sure that the subset roles are created in the order of the preferred readings.
555
568
  role_sequences = readings_list.map{|r| @constellation.RoleSequence(:new) }
556
569
  common_bindings.each_with_index do |binding, index|
557
570
  role_sequences.each_with_index do |rs, rsi|
@@ -572,6 +585,8 @@ module ActiveFacts
572
585
 
573
586
  #puts "subset_constraint:\n\t#{subset_readings.inspect}\n\t#{superset_readings.inspect}"
574
587
  #puts "\t#{role_sequences.map{|rs| rs.describe}.inspect}"
588
+ #puts "subset_role_sequence = #{role_sequences[0].describe}"
589
+ #puts "superset_role_sequence = #{role_sequences[1].describe}"
575
590
 
576
591
  # create the constraint:
577
592
  constraint = @constellation.SubsetConstraint(:new)
@@ -591,8 +606,8 @@ module ActiveFacts
591
606
  # Create the constraint:
592
607
  constraint = @constellation.SetExclusionConstraint(:new)
593
608
  constraint.vocabulary = @vocabulary
594
- role_sequences.each do |rs|
595
- @constellation.SetComparisonRoles(constraint, rs)
609
+ role_sequences.each_with_index do |rs, i|
610
+ @constellation.SetComparisonRoles(constraint, i, :role_sequence => rs)
596
611
  end
597
612
  constraint.is_mandatory = quantifier[0] == 1
598
613
  end
@@ -605,8 +620,8 @@ module ActiveFacts
605
620
  # Create the constraint:
606
621
  constraint = @constellation.SetEqualityConstraint(:new)
607
622
  constraint.vocabulary = @vocabulary
608
- role_sequences.each do |rs|
609
- @constellation.SetComparisonRoles(constraint, rs)
623
+ role_sequences.each_with_index do |rs, i|
624
+ @constellation.SetComparisonRoles(constraint, i, :role_sequence => rs)
610
625
  end
611
626
  end
612
627
 
@@ -666,6 +681,9 @@ module ActiveFacts
666
681
  rr.role = role
667
682
  end
668
683
  #puts "New external PresenceConstraint with quantifier = #{quantifier.inspect} over #{rs.describe}"
684
+
685
+ # REVISIT: Check that no existing PC spans the same roles (nor a superset nor subset?)
686
+
669
687
  @constellation.PresenceConstraint(
670
688
  :new,
671
689
  :name => '',
@@ -679,6 +697,8 @@ module ActiveFacts
679
697
  )
680
698
  end
681
699
 
700
+ # Search the supertypes of 'subtype' looking for an inheritance path to 'supertype',
701
+ # and returning the array of TypeInheritance fact types from supertype to subtype.
682
702
  def inheritance_path(subtype, supertype)
683
703
  direct_inheritance = subtype.all_supertype_inheritance.select{|ti| ti.supertype == supertype}
684
704
  return direct_inheritance if (direct_inheritance[0])
@@ -692,12 +712,16 @@ module ActiveFacts
692
712
  # For a given reading from the parser, find the matching declared reading, and return
693
713
  # the array of Role object in the same order as they occur in the reading.
694
714
  def invoked_fact_roles(reading)
715
+ # REVISIT: Possibly this special reading from the parser can be removed now?
695
716
  if (reading[0] == "!SUBTYPE!")
696
717
  subtype = reading[1][:binding].concept
697
718
  supertype = reading[2][:binding].concept
698
719
  raise "#{subtype.name} is not a subtype of #{supertype.name}" unless subtype.supertypes_transitive.include?(supertype)
699
720
  ip = inheritance_path(subtype, supertype)
700
- return [ip[0].all_role[0], ip[-1].all_role[1]]
721
+ return [
722
+ ip[-1].all_role.detect{|r| r.concept == subtype},
723
+ ip[0].all_role.detect{|r| r.concept == supertype}
724
+ ]
701
725
  end
702
726
 
703
727
  bindings = reading.select{|p| Hash === p}
@@ -721,7 +745,7 @@ module ActiveFacts
721
745
  to_match = reading.clone
722
746
  players_to_match = players.clone
723
747
  candidate_reading.words_and_role_refs.each do |wrr|
724
- if (RoleRef === wrr)
748
+ if (wrr.is_a?(RoleRef))
725
749
  break unless Hash === to_match.first
726
750
  break unless binding = to_match[0][:binding]
727
751
  # REVISIT: May need to match super- or sub-types here too!
@@ -745,7 +769,7 @@ module ActiveFacts
745
769
  # we need to accumulate all possible matches to be sure
746
770
  # there's only one, or the match is exact, or risk ambiguity.
747
771
  debug "Reading match was #{to_match.size == 0 ? "ok" : "bad"}"
748
- return candidate_reading.role_sequence.all_role_ref.map{|rr| rr.role} if to_match.size == 0
772
+ return candidate_reading.role_sequence.all_role_ref.sort_by{|rr| rr.ordinal}.map{|rr| rr.role} if to_match.size == 0
749
773
  end
750
774
  end
751
775
  end
@@ -753,7 +777,7 @@ module ActiveFacts
753
777
 
754
778
  # Hmm, that didn't work, try the subtypes of the first player.
755
779
  # When a fact type matches like this, there is an implied join to the subtype.
756
- players[0].subtypes.map do |subtype|
780
+ players[0].subtypes.each do |subtype|
757
781
  players[0] = subtype
758
782
  fr = invoked_fact_roles_by_players(reading, players)
759
783
  return fr if fr
@@ -831,7 +855,9 @@ module ActiveFacts
831
855
 
832
856
  def fact_type_identification(fact_type, name, prefer)
833
857
  if !@symbols.embedded_presence_constraints.detect{|pc| pc.max_frequency == 1}
834
- first_role_sequence = fact_type.all_reading[0].role_sequence
858
+ # Provide a default identifier for a fact type that's lacking one (over all roles):
859
+ first_role_sequence = fact_type.preferred_reading.role_sequence
860
+ #puts "Creating PC for #{name}: #{fact_type.describe}"
835
861
  identifier = @constellation.PresenceConstraint(
836
862
  :new,
837
863
  :vocabulary => @vocabulary,
@@ -887,6 +913,7 @@ module ActiveFacts
887
913
  constraint = find_pc_over_roles(constrained_roles)
888
914
  if constraint
889
915
  debug "Setting max frequency to #{quantifier[1]} for existing constraint #{constraint.object_id} over #{constraint.role_sequence.describe} in #{fact_type.describe}"
916
+ raise "Conflicting maximum frequency for constraint" if constraint.max_frequency && constraint.max_frequency != quantifier[1]
890
917
  constraint.max_frequency = quantifier[1]
891
918
  else
892
919
  role_sequence = @constellation.RoleSequence(:new)
@@ -917,11 +944,11 @@ module ActiveFacts
917
944
  ring_constraints, qualifiers = qualifiers.partition{|q| RingTypes.include?(q) }
918
945
  unless ring_constraints.empty?
919
946
  # A Ring may be over a supertype/subtype pair, and this won't find that.
920
- role_refs = role_sequence.all_role_ref
947
+ role_refs = Array(role_sequence.all_role_ref)
921
948
  role_pairs = []
922
949
  player_supertypes_by_role = role_refs.map{|rr|
923
950
  concept = rr.role.concept
924
- EntityType === concept ? supertypes(concept) : [concept]
951
+ concept.is_a?(EntityType) ? supertypes(concept) : [concept]
925
952
  }
926
953
  role_refs.each_with_index{|rr1, i|
927
954
  player1 = rr1.role.concept
@@ -967,7 +994,7 @@ module ActiveFacts
967
994
  return nil if roles.size == 0 # Safeguard; this would chuck an exception otherwise
968
995
  roles[0].all_role_ref.each do |role_ref|
969
996
  next if role_ref.role_sequence.all_role_ref.map(&:role) != roles
970
- pc = role_ref.role_sequence.all_presence_constraint[0]
997
+ pc = role_ref.role_sequence.all_presence_constraint.only # Will throw an exception if there's more than one.
971
998
  #puts "Existing PresenceConstraint matches those roles!" if pc
972
999
  return pc if pc
973
1000
  end
@@ -1,7 +1,8 @@
1
1
  #
2
- # Read a NORMA file into an ActiveFacts vocabulary
2
+ # ActiveFacts Vocabulary Input.
3
+ # Read a NORMA file into an ActiveFacts vocabulary
3
4
  #
4
- # Copyright (c) 2008 Clifford Heath. Read the LICENSE file.
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
5
6
  #
6
7
  # This code uses variables prefixed with x_ when they refer to Rexml nodes.
7
8
  # Every node having an id="..." is indexed in @x_by_id[] hash before processing.
@@ -13,10 +14,12 @@ require 'activefacts/vocabulary'
13
14
 
14
15
  module ActiveFacts
15
16
  module Input
16
- # The ORM Input module is activated whenever afgen is called upon to process a file
17
- # whose name ends in .orm (a file from NORMA). This parser uses Rexml so it's very slow.
18
- # The file is parsed to a constellation and the vocabulary object defined in that file is returned.
17
+ # Compile a NORMA (.orm) file to an ActiveFacts vocabulary.
18
+ # Invoke as
19
+ # afgen --<generator> <file>.orm
20
+ # This parser uses Rexml so it's very slow.
19
21
  class ORM
22
+ private
20
23
  def self.readfile(filename)
21
24
  File.open(filename) {|file|
22
25
  self.read(file, filename)
@@ -32,7 +35,8 @@ module ActiveFacts
32
35
  @filename = filename
33
36
  end
34
37
 
35
- def read
38
+ public
39
+ def read #:nodoc:
36
40
  begin
37
41
  @document = REXML::Document.new(@file)
38
42
  rescue => e
@@ -56,6 +60,8 @@ module ActiveFacts
56
60
  @vocabulary
57
61
  end
58
62
 
63
+ private
64
+
59
65
  def read_vocabulary
60
66
  @constellation = ActiveFacts::API::Constellation.new(ActiveFacts::Metamodel)
61
67
  @vocabulary = @constellation.Vocabulary(@x_model.attributes['Name'])
@@ -420,8 +426,9 @@ module ActiveFacts
420
426
  end
421
427
 
422
428
  def extract_adjectives(reading_text, role_sequence)
423
- (0...role_sequence.all_role_ref.size).each{|i|
424
- role_ref = role_sequence.all_role_ref[i]
429
+ all_role_refs = role_sequence.all_role_ref.sort_by{|rr| rr.ordinal}
430
+ (0...all_role_refs.size).each{|i|
431
+ role_ref = all_role_refs[i]
425
432
  role = role_ref.role
426
433
 
427
434
  word = '\b([A-Za-z][A-Za-z0-9_]*)\b'
@@ -455,7 +462,7 @@ module ActiveFacts
455
462
  # REVISIT: This searches all role sequences. Perhaps we could narrow it down first instead?
456
463
  role_sequence = @constellation.RoleSequence.values.detect{|c|
457
464
  #puts "Checking RoleSequence [#{c.all_role_ref.map{|rr| rr.role.concept.name}*", "}]"
458
- role_array == c.all_role_ref.map{|rr| rr.role }
465
+ role_array == c.all_role_ref.sort_by{|rr| rr.ordinal}.map{|rr| rr.role }
459
466
  }
460
467
  # puts "Found matching RoleSequence!" if role_sequence
461
468
  return role_sequence if role_sequence
@@ -622,12 +629,15 @@ module ActiveFacts
622
629
  # (pi ? " (preferred id for #{pi.name})" : "") +
623
630
  # (mc ? " (mandatory)" : "") if pi && !mc
624
631
 
625
- # A TypeInheritance fact type has a UC on each role.
626
- # If it's on the identification path, mark it as preferred identifier
627
- role = roles.all_role_ref[0].role
628
- supertype_constraint = role.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance) &&
629
- role.concept == role.fact_type.supertype &&
630
- role.fact_type.provides_identification
632
+ # A TypeInheritance fact type has a uniqueness constraint on each role.
633
+ # If this UC is on the supertype and identifies the subtype, it's preferred:
634
+ is_supertype_constraint =
635
+ roles.all_role_ref.size == 1 &&
636
+ (role = roles.all_role_ref.only.role) &&
637
+ (fact_type = role.fact_type) &&
638
+ fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance) &&
639
+ role.concept == fact_type.supertype &&
640
+ fact_type.provides_identification
631
641
 
632
642
  pc = @constellation.PresenceConstraint(:new)
633
643
  pc.vocabulary = @vocabulary
@@ -636,7 +646,7 @@ module ActiveFacts
636
646
  pc.is_mandatory = true if mc
637
647
  pc.min_frequency = mc ? 1 : 0
638
648
  pc.max_frequency = 1
639
- pc.is_preferred_identifier = true if pi || unary_identifier || supertype_constraint
649
+ pc.is_preferred_identifier = true if pi || unary_identifier || is_supertype_constraint
640
650
  #puts "#{name} covers #{roles.describe} has min=#{pc.min_frequency}, max=1, preferred=#{pc.is_preferred_identifier.inspect}" if emit_special_debug
641
651
 
642
652
  #puts roles.verbalise
@@ -673,9 +683,9 @@ module ActiveFacts
673
683
  ec.vocabulary = @vocabulary
674
684
  ec.name = name
675
685
  # ec.enforcement =
676
- role_sequences.each{|rs|
677
- @constellation.SetComparisonRoles(ec, rs)
678
- }
686
+ role_sequences.each_with_index do |rs, i|
687
+ @constellation.SetComparisonRoles(ec, i, :role_sequence => rs)
688
+ end
679
689
  ec.is_mandatory = true if x_mandatory
680
690
  }
681
691
  end
@@ -699,9 +709,9 @@ module ActiveFacts
699
709
  ec.vocabulary = @vocabulary
700
710
  ec.name = name
701
711
  # ec.enforcement =
702
- role_sequences.each{|rs|
703
- @constellation.SetComparisonRoles(ec, rs)
704
- }
712
+ role_sequences.each_with_index do |rs, i|
713
+ @constellation.SetComparisonRoles(ec, i, :role_sequence => rs)
714
+ end
705
715
  }
706
716
  end
707
717