activefacts 0.6.0

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 (84) hide show
  1. data/History.txt +4 -0
  2. data/Manifest.txt +83 -0
  3. data/README.rdoc +81 -0
  4. data/Rakefile +41 -0
  5. data/bin/afgen +46 -0
  6. data/bin/cql +52 -0
  7. data/examples/CQL/Address.cql +46 -0
  8. data/examples/CQL/Blog.cql +54 -0
  9. data/examples/CQL/CompanyDirectorEmployee.cql +51 -0
  10. data/examples/CQL/Death.cql +16 -0
  11. data/examples/CQL/Genealogy.cql +95 -0
  12. data/examples/CQL/Marriage.cql +18 -0
  13. data/examples/CQL/Metamodel.cql +238 -0
  14. data/examples/CQL/MultiInheritance.cql +19 -0
  15. data/examples/CQL/OilSupply.cql +47 -0
  16. data/examples/CQL/Orienteering.cql +108 -0
  17. data/examples/CQL/PersonPlaysGame.cql +17 -0
  18. data/examples/CQL/SchoolActivities.cql +31 -0
  19. data/examples/CQL/SimplestUnary.cql +12 -0
  20. data/examples/CQL/SubtypePI.cql +32 -0
  21. data/examples/CQL/Warehousing.cql +99 -0
  22. data/examples/CQL/WindowInRoomInBldg.cql +22 -0
  23. data/lib/activefacts.rb +10 -0
  24. data/lib/activefacts/api.rb +25 -0
  25. data/lib/activefacts/api/concept.rb +384 -0
  26. data/lib/activefacts/api/constellation.rb +106 -0
  27. data/lib/activefacts/api/entity.rb +239 -0
  28. data/lib/activefacts/api/instance.rb +54 -0
  29. data/lib/activefacts/api/numeric.rb +158 -0
  30. data/lib/activefacts/api/role.rb +94 -0
  31. data/lib/activefacts/api/standard_types.rb +67 -0
  32. data/lib/activefacts/api/support.rb +59 -0
  33. data/lib/activefacts/api/value.rb +122 -0
  34. data/lib/activefacts/api/vocabulary.rb +120 -0
  35. data/lib/activefacts/cql.rb +31 -0
  36. data/lib/activefacts/cql/CQLParser.treetop +104 -0
  37. data/lib/activefacts/cql/Concepts.treetop +112 -0
  38. data/lib/activefacts/cql/DataTypes.treetop +66 -0
  39. data/lib/activefacts/cql/Expressions.treetop +113 -0
  40. data/lib/activefacts/cql/FactTypes.treetop +185 -0
  41. data/lib/activefacts/cql/Language/English.treetop +92 -0
  42. data/lib/activefacts/cql/LexicalRules.treetop +169 -0
  43. data/lib/activefacts/cql/Rakefile +6 -0
  44. data/lib/activefacts/cql/parser.rb +88 -0
  45. data/lib/activefacts/generate/absorption.rb +87 -0
  46. data/lib/activefacts/generate/cql.rb +441 -0
  47. data/lib/activefacts/generate/cql/html.rb +397 -0
  48. data/lib/activefacts/generate/null.rb +19 -0
  49. data/lib/activefacts/generate/ordered.rb +557 -0
  50. data/lib/activefacts/generate/ruby.rb +326 -0
  51. data/lib/activefacts/generate/sql/server.rb +164 -0
  52. data/lib/activefacts/generate/text.rb +21 -0
  53. data/lib/activefacts/input/cql.rb +1268 -0
  54. data/lib/activefacts/input/orm.rb +926 -0
  55. data/lib/activefacts/persistence.rb +1 -0
  56. data/lib/activefacts/persistence/composition.rb +653 -0
  57. data/lib/activefacts/support.rb +51 -0
  58. data/lib/activefacts/version.rb +3 -0
  59. data/lib/activefacts/vocabulary.rb +6 -0
  60. data/lib/activefacts/vocabulary/extensions.rb +343 -0
  61. data/lib/activefacts/vocabulary/metamodel.rb +303 -0
  62. data/script/txt2html +71 -0
  63. data/spec/absorption_spec.rb +95 -0
  64. data/spec/api/autocounter.rb +82 -0
  65. data/spec/api/constellation.rb +130 -0
  66. data/spec/api/entity_type.rb +101 -0
  67. data/spec/api/instance.rb +428 -0
  68. data/spec/api/roles.rb +122 -0
  69. data/spec/api/value_type.rb +112 -0
  70. data/spec/api_spec.rb +14 -0
  71. data/spec/cql_cql_spec.rb +58 -0
  72. data/spec/cql_parse_spec.rb +31 -0
  73. data/spec/cql_ruby_spec.rb +60 -0
  74. data/spec/cql_sql_spec.rb +54 -0
  75. data/spec/cql_symbol_tables_spec.rb +259 -0
  76. data/spec/cql_unit_spec.rb +336 -0
  77. data/spec/cqldump_spec.rb +169 -0
  78. data/spec/norma_cql_spec.rb +48 -0
  79. data/spec/norma_ruby_spec.rb +50 -0
  80. data/spec/norma_sql_spec.rb +45 -0
  81. data/spec/norma_tables_spec.rb +94 -0
  82. data/spec/spec.opts +1 -0
  83. data/spec/spec_helper.rb +10 -0
  84. metadata +173 -0
@@ -0,0 +1,92 @@
1
+ module ActiveFacts
2
+ module CQL
3
+ grammar Language
4
+
5
+ rule subtype_prefix
6
+ defined_as 'subtype' S 'of' S
7
+ / 'is' s 'a' s ('kind'/'subtype') s 'of' S
8
+ end
9
+
10
+ rule defined_as
11
+ s 'is' s 'defined' S as s
12
+ end
13
+
14
+ rule identified_by
15
+ identified s by s
16
+ end
17
+
18
+ rule quantifier
19
+ each s { def value; [1, nil]; end }
20
+ / some s { def value; nil; end }
21
+ # REVISIT: "Some" means that any prior occurrence of this role player is to be ignored; this is a new occurrence
22
+ # "that" on the other hand means that this role player was *previously* designated using "some".
23
+ # These distinctions are lost here
24
+ / that s { def value; nil; end }
25
+ / one s { def value; [1, 1]; end }
26
+ / no s { def value; [0, 0]; end }
27
+ / exactly s quantity { def value; q = quantity.value; [q, q]; end }
28
+ / at s least s quantity most:( and s at s most s q:quantity )?
29
+ { def value;
30
+ [ quantity.value,
31
+ most.empty? ? nil : most.q.value
32
+ ]
33
+ end
34
+ }
35
+ / at s most s quantity { def value; [ nil, quantity.value ]; end }
36
+ / from s numeric_range s { def value; numeric_range.value; end }
37
+ / either_all_or_none s { def value; [ -1, 1 ]; end }
38
+ end
39
+
40
+ rule quantity
41
+ one s { def value; 1; end }
42
+ / number s { def value; number.value; end }
43
+ end
44
+
45
+ rule acyclic 'acyclic' !alphanumeric end
46
+ rule alias 'alias' !alphanumeric end
47
+ rule all 'all' !alphanumeric end
48
+ rule and 'and' !alphanumeric end
49
+ rule as 'as' !alphanumeric end
50
+ rule at 'at' !alphanumeric end
51
+ rule by 'by' !alphanumeric end
52
+ rule definitely 'definitely' !alphanumeric end
53
+ rule each 'each' !alphanumeric end
54
+ rule either 'either' !alphanumeric end
55
+ rule entity 'entity' !alphanumeric end
56
+ rule exactly 'exactly' !alphanumeric end
57
+ rule false 'false' !alphanumeric end
58
+ rule from 'from' !alphanumeric end
59
+ rule identified ('known'/'identified') !alphanumeric end
60
+ rule if 'if' !alphanumeric end
61
+ rule import 'import' !alphanumeric end
62
+ rule includes 'includes' !alphanumeric end
63
+ rule intransitive 'intransitive' !alphanumeric end
64
+ rule is 'is' !alphanumeric end
65
+ rule its 'its' !alphanumeric end
66
+ rule least 'least' !alphanumeric end
67
+ rule matches 'matches' !alphanumeric end
68
+ rule maybe 'maybe' !alphanumeric end
69
+ rule most 'most' !alphanumeric end
70
+ rule no 'no' !alphanumeric end
71
+ rule none 'none' !alphanumeric end
72
+ rule not 'not' !alphanumeric end
73
+ rule either_all_or_none either s all s or s none end
74
+ rule one 'one' !alphanumeric end
75
+ rule only 'only' !alphanumeric end
76
+ rule or 'or' !alphanumeric end
77
+ rule restricted 'restricted' !alphanumeric end
78
+ rule returning 'returning' !alphanumeric end
79
+ rule some 'some' !alphanumeric end
80
+ rule static 'static' !alphanumeric end
81
+ rule symmetric 'symmetric' !alphanumeric end
82
+ rule that 'that' !alphanumeric end
83
+ rule to 'to' !alphanumeric end
84
+ rule transient 'transient' !alphanumeric end
85
+ rule transitive 'transitive' !alphanumeric end
86
+ rule true 'true' !alphanumeric end
87
+ rule vocabulary 'vocabulary' !alphanumeric end
88
+ rule where 'where' !alphanumeric end
89
+
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,169 @@
1
+ module ActiveFacts
2
+ module CQL
3
+ grammar LexicalRules
4
+
5
+ rule range
6
+ numeric_range / string_range
7
+ end
8
+
9
+ rule numeric_range
10
+ number s tail:( '..' s end:number? s )?
11
+ {
12
+ def value
13
+ if !tail.empty?
14
+ last = tail.end.value unless tail.end.empty?
15
+ [ number.value, last ]
16
+ else
17
+ number.value
18
+ end
19
+ end
20
+ }
21
+ / '..' s number s
22
+ {
23
+ def value
24
+ [ nil, number.value ]
25
+ end
26
+ }
27
+ end
28
+
29
+ rule string_range
30
+ string s tail:( '..' s end:string? s )?
31
+ {
32
+ # Ranges require the original text of the string, not the content:
33
+ def value
34
+ first = string.text_value
35
+ if !tail.empty?
36
+ last = tail.end.text_value unless tail.end.empty?
37
+ [ first, last ]
38
+ else
39
+ first
40
+ end
41
+ end
42
+ }
43
+ / '..' s string s
44
+ {
45
+ def value
46
+ [ nil, string.value ]
47
+ end
48
+ }
49
+ end
50
+
51
+ rule literal
52
+ ( boolean_literal
53
+ / string
54
+ / number
55
+ ) s
56
+ {
57
+ def value
58
+ elements[0].value
59
+ end
60
+ }
61
+ end
62
+
63
+ rule boolean_literal
64
+ ( true { def value; true; end }
65
+ / false { def value; false; end }
66
+ ) !alphanumeric
67
+ {
68
+ def value; elements[0].value end
69
+ }
70
+ end
71
+
72
+ rule string
73
+ "'" (string_char)* "'"
74
+ {
75
+ def value
76
+ text_value
77
+ eval(text_value.sub(/\A'(.*)'\Z/,'"\1"'))
78
+ end
79
+ }
80
+ end
81
+
82
+ rule number
83
+ ( real /
84
+ fractional_real /
85
+ hexnumber /
86
+ octalnumber
87
+ ) !alphanumeric
88
+ {
89
+ def value
90
+ eval(text_value)
91
+ end
92
+ }
93
+ end
94
+
95
+ # All purely lexical rules from here down, no-one looks at the structure, just the text_value:
96
+
97
+ rule string_char
98
+ ( '\\' [befntr\\']
99
+ / '\\' [0-7] [0-7] [0-7]
100
+ / '\\0'
101
+ / '\\x' [0-9A-Fa-f] [0-9A-Fa-f]
102
+ / '\\u' [0-9A-Fa-f] [0-9A-Fa-f] [0-9A-Fa-f] [0-9A-Fa-f]
103
+ / (![\'\\\0-\x07\x0A-\x1F] .)
104
+ )
105
+ end
106
+
107
+ rule real
108
+ [-+]? [1-9] [0-9]* fraction? exponent?
109
+ end
110
+
111
+ rule fractional_real
112
+ [-+]? '0' fraction exponent?
113
+ end
114
+
115
+ rule fraction
116
+ '.' [0-9]+
117
+ end
118
+
119
+ rule exponent
120
+ ( [Ee] '-'? [0-9]+ )
121
+ end
122
+
123
+ rule hexnumber
124
+ '0x' [0-9A-Fa-f]+
125
+ end
126
+
127
+ rule octalnumber
128
+ '0' [0-7]*
129
+ end
130
+
131
+ rule mul_op
132
+ '/' / '%' / '*'
133
+ end
134
+
135
+ rule id
136
+ alpha alphanumeric*
137
+ end
138
+
139
+ rule alpha
140
+ [A-Za-z_]
141
+ end
142
+
143
+ rule alphanumeric
144
+ alpha / [0-9]
145
+ end
146
+
147
+ rule s # Optional space
148
+ S?
149
+ end
150
+
151
+ rule S # Mandatory space
152
+ (white / comment_to_eol / comment_c_style)+
153
+ end
154
+
155
+ rule white
156
+ [ \t\n\r]+
157
+ end
158
+
159
+ rule comment_to_eol
160
+ '//' (!"\n" .)+
161
+ end
162
+
163
+ rule comment_c_style
164
+ '/*' (!'*/' . )* '*/'
165
+ end
166
+
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,6 @@
1
+ task :default do
2
+ pattern = File.dirname(__FILE__) + '**/*.treetop'
3
+ files = Dir[pattern]
4
+ # Hopefully this quoting will work where there are spaces in filenames, and even maybe on Windows?
5
+ exec "tt '#{files*"' '"}'"
6
+ end
@@ -0,0 +1,88 @@
1
+ #
2
+ # ActiveFacts CQL parser and loader.
3
+ # Copyright (c) 2008 Clifford Heath. Read the LICENSE file.
4
+ #
5
+ require 'rubygems'
6
+ require 'treetop'
7
+
8
+ # These are Treetop files, which it will compile on the fly if precompiled ones aren't found:
9
+ require 'activefacts/cql/LexicalRules'
10
+ require 'activefacts/cql/Language/English'
11
+ require 'activefacts/cql/Expressions'
12
+ require 'activefacts/cql/Concepts'
13
+ require 'activefacts/cql/DataTypes'
14
+ require 'activefacts/cql/FactTypes'
15
+ require 'activefacts/cql/CQLParser'
16
+
17
+ module ActiveFacts
18
+ # Extend the generated parser:
19
+ class CQLParser
20
+ include ActiveFacts
21
+
22
+ # Repeatedly parse rule_name until all input is consumed,
23
+ # returning an array of syntax trees for each definition.
24
+ def parse_all(input, rule_name = nil, &block)
25
+ self.root = rule_name if rule_name
26
+
27
+ @index = 0 # Byte offset to start next parse
28
+ self.consume_all_input = false
29
+ results = []
30
+ begin
31
+ node = parse(input, :index => @index)
32
+ return nil unless node
33
+ node = block.call(node) if block
34
+ results << node if node
35
+ end until self.index == @input_length
36
+ results
37
+ end
38
+
39
+ def definition(node)
40
+ name, ast = *node.value
41
+ kind, *value = *ast
42
+
43
+ begin
44
+ debug "CQL: Processing definition #{[kind, name].compact*" "}" do
45
+ case kind
46
+ when :vocabulary
47
+ [kind, name]
48
+ when :data_type
49
+ data_type(name, value)
50
+ when :entity_type
51
+ supertypes = value.shift
52
+ entity_type(name, supertypes, value)
53
+ when :fact_type
54
+ f = fact_type(name, value)
55
+ when :constraint
56
+ ast
57
+ end
58
+ end
59
+ end
60
+ rescue => e
61
+ raise "in #{kind.to_s.camelcase(true)} definition, #{e.message}:\n\t#{node.text_value}"
62
+ end
63
+
64
+ def data_type(name, value)
65
+ # REVISIT: Massage/check data type here?
66
+ [:data_type, name, *value]
67
+ end
68
+
69
+ def entity_type(name, supertypes, value)
70
+ #print "entity_type parameters for #{name}: "; p value
71
+ identification, clauses = *value
72
+ clauses ||= []
73
+
74
+ # raise "Entity type clauses must all be fact types" if clauses.detect{|c| c[0] != :fact_clause }
75
+
76
+ [:entity_type, name, supertypes, identification, clauses]
77
+ end
78
+
79
+ def fact_type(name, value)
80
+ defined_readings, *clauses = value
81
+
82
+ [:fact_type, name, defined_readings, clauses]
83
+ end
84
+
85
+ end
86
+
87
+ Polyglot.register('cql', CQLParser)
88
+ end
@@ -0,0 +1,87 @@
1
+ #
2
+ # Generate text output for ActiveFacts vocabularies.
3
+ #
4
+ # Copyright (c) 2007 Clifford Heath. Read the LICENSE file.
5
+ # Author: Clifford Heath.
6
+ #
7
+ require 'activefacts/persistence'
8
+
9
+ module ActiveFacts
10
+ module Generate
11
+ class ABSORPTION
12
+ include Metamodel
13
+
14
+ def initialize(vocabulary, *options)
15
+ @vocabulary = vocabulary
16
+ @vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::Constellation === @vocabulary
17
+ @no_columns = options.include? "no_columns"
18
+ @dependent = options.include? "dependent"
19
+ @paths = options.include? "paths"
20
+ @no_identifier = options.include? "no_identifier"
21
+ end
22
+
23
+ def generate(out = $>)
24
+ no_absorption = 0
25
+ single_absorption_vts = 0
26
+ single_absorption_ets = 0
27
+ multi_absorption_vts = 0
28
+ multi_absorption_ets = 0
29
+ @vocabulary.tables
30
+ @vocabulary.all_feature.sort_by{|c| c.name}.each do |o|
31
+ # Don't dump imported (base) ValueTypes:
32
+ next if ValueType === o && !o.supertype
33
+ show(o)
34
+
35
+ case o.absorption_paths.size
36
+ when 0; no_absorption += 1
37
+ when 1
38
+ if ValueType === o
39
+ single_absorption_vts += 1
40
+ else
41
+ single_absorption_ets += 1
42
+ end
43
+ else
44
+ if ValueType === o
45
+ multi_absorption_vts += 1
46
+ else
47
+ multi_absorption_ets += 1
48
+ end
49
+ end
50
+
51
+ end
52
+ puts "#{no_absorption} concepts have no absorption paths, #{single_absorption_vts}/#{single_absorption_ets} value/entity types have only one path, #{multi_absorption_vts}/#{multi_absorption_ets} have more than one"
53
+ end
54
+
55
+ def show concept
56
+ return unless concept.independent || @dependent
57
+
58
+ print "#{concept.name}"
59
+ print " (#{concept.tentative ? "tentatively " : ""}#{concept.independent ? "in" : ""}dependent)" if @dependent
60
+
61
+ if !@no_identifier && concept.is_a?(EntityType)
62
+ print " is identified by:\n\t#{
63
+ concept.absorbed_reference_roles.all_role_ref.map { |rr| rr.column_name(".") } * ",\n\t"
64
+ }"
65
+ end
66
+ print "\n"
67
+
68
+ unless @no_columns
69
+ puts "#{ concept.absorbed_roles.all_role_ref.map do |role_ref|
70
+ "\t#{role_ref.column_name(".")}\n"
71
+ end*"" }"
72
+ end
73
+
74
+ if (@paths)
75
+ ap = concept.absorption_paths
76
+ puts "#{ ap.map {|role|
77
+ prr = role.preferred_reference.describe
78
+ player = role.fact_type.entity_type == concept ? role.concept : (role.fact_type.all_role-[role])[0].concept
79
+ "\tcan absorb #{prr != role.concept.name ? "(via #{prr}) " : "" }into #{player.name}\n"
80
+ }*"" }"
81
+ end
82
+
83
+ end
84
+ end
85
+ end
86
+ end
87
+
@@ -0,0 +1,441 @@
1
+ #
2
+ # Generate CQL from an ActiveFacts vocabulary.
3
+ # Copyright (c) 2008 Clifford Heath. Read the LICENSE file.
4
+ #
5
+ require 'activefacts/vocabulary'
6
+ require 'activefacts/generate/ordered'
7
+
8
+ module ActiveFacts
9
+ module Generate #:nodoc:
10
+ class CQL < OrderedDumper
11
+ include Metamodel
12
+
13
+ def vocabulary_start(vocabulary)
14
+ puts "vocabulary #{vocabulary.name};\n\n"
15
+ end
16
+
17
+ def vocabulary_end
18
+ end
19
+
20
+ def value_type_banner
21
+ puts "/*\n * Value Types\n */"
22
+ end
23
+
24
+ def value_type_end
25
+ puts "\n"
26
+ end
27
+
28
+ def value_type_dump(o)
29
+ return unless o.supertype # An imported type
30
+ if o.name == o.supertype.name
31
+ # In ActiveFacts, parameterising a ValueType will create a new datatype
32
+ # throw Can't handle parameterized value type of same name as its datatype" if ...
33
+ end
34
+
35
+ parameters =
36
+ [ o.length != 0 || o.scale != 0 ? o.length : nil,
37
+ o.scale != 0 ? o.scale : nil
38
+ ].compact
39
+ parameters = parameters.length > 0 ? "("+parameters.join(",")+")" : "()"
40
+
41
+ #" restricted to {#{(allowed_values.map{|r| r.inspect}*", ").gsub('"',"'")}}")
42
+
43
+ puts "#{o.name} is defined as #{o.supertype.name}#{ parameters }#{
44
+ o.value_restriction ? " restricted to {#{
45
+ o.value_restriction.all_allowed_range.map{|ar|
46
+ # REVISIT: Need to display as string or numeric according to type here...
47
+ min = ar.value_range.minimum_bound
48
+ max = ar.value_range.maximum_bound
49
+
50
+ (min ? min.value : "") +
51
+ (min.value != (max&&max.value) ? (".." + (max ? max.value : "")) : "")
52
+ }*", "
53
+ }}" : ""
54
+ };"
55
+ end
56
+
57
+ def append_ring_to_reading(reading, ring)
58
+ reading << " [#{(ring.ring_type.scan(/[A-Z][a-z]*/)*", ").downcase}]"
59
+ end
60
+
61
+ def identified_by_roles_and_facts(entity_type, identifying_roles, identifying_facts, preferred_readings)
62
+ identifying_role_names = identifying_roles.map{|role|
63
+ preferred_role_ref = preferred_readings[role.fact_type].role_sequence.all_role_ref.detect{|reading_rr|
64
+ reading_rr.role == role
65
+ }
66
+ role_words = []
67
+ # REVISIT: Consider whether NOT to use the adjective if it's a prefix of the role_name
68
+
69
+ role_name = role.role_name
70
+ role_name = nil if role_name == ""
71
+ # debug "concept.name=#{preferred_role_ref.role.concept.name}, role_name=#{role_name.inspect}, preferred_role_name=#{preferred_role_ref.role.role_name.inspect}"
72
+
73
+ if (role.fact_type.all_role.size == 1)
74
+ # REVISIT: Guard against unary reading containing the illegal words "and" and "where".
75
+ role.fact_type.default_reading # Need whole reading for a unary.
76
+ elsif (role_name)
77
+ role_name
78
+ else
79
+ role_words << preferred_role_ref.leading_adjective if preferred_role_ref.leading_adjective != ""
80
+ role_words << preferred_role_ref.role.concept.name
81
+ role_words << preferred_role_ref.trailing_adjective if preferred_role_ref.trailing_adjective != ""
82
+ role_words.compact*"-"
83
+ end
84
+ }
85
+
86
+ # REVISIT: Consider emitting extra fact types here, instead of in entity_type_dump?
87
+ # Just beware that readings having the same players will be considered to be of the same fact type, even if they're not.
88
+
89
+ # Detect standard reference-mode scenarios
90
+ ft = identifying_facts[0]
91
+ fact_constraints = nil
92
+ if identifying_facts.size == 1 and
93
+ entity_role = ft.all_role[n = (ft.all_role[0].concept == entity_type ? 0 : 1)] and
94
+ value_role = ft.all_role[1-n] and
95
+ value_name = value_role.concept.name and
96
+ residual = value_name.gsub(%r{#{entity_role.concept.name}},'') and
97
+ residual != '' and
98
+ residual != value_name
99
+
100
+ # The EntityType is identified by its association with a single ValueType
101
+ # whose name is an extension (the residual) of the EntityType's name.
102
+
103
+ # Detect standard reference-mode readings:
104
+ forward_reading = reverse_reading = nil
105
+ ft.all_reading.each do |reading|
106
+ if reading.reading_text =~ /^\{(\d)\} has \{\d\}$/
107
+ if reading.role_sequence.all_role_ref[$1.to_i].role == entity_role
108
+ forward_reading = reading
109
+ else
110
+ reverse_reading = reading
111
+ end
112
+ elsif reading.reading_text =~ /^\{(\d)\} is of \{\d\}$/
113
+ if reading.role_sequence.all_role_ref[$1.to_i].role == value_role
114
+ reverse_reading = reading
115
+ else
116
+ forward_reading = reading
117
+ end
118
+ end
119
+ end
120
+
121
+ debug :mode, "------------------- Didn't find standard forward reading" unless forward_reading
122
+ debug :mode, "------------------- Didn't find standard reverse reading" unless reverse_reading
123
+
124
+ # If we didn't find at least one of the standard readings, don't use a refmode:
125
+ if (forward_reading || reverse_reading)
126
+ # Elide the constraints that would have been emitted on those readings.
127
+ # If there is a UC that's not in the standard form for a reference mode,
128
+ # we have to emit the standard reading anyhow.
129
+ fact_constraints = @presence_constraints_by_fact[ft]
130
+ fact_constraints.each do |pc|
131
+ if (pc.role_sequence.all_role_ref.size == 1 and pc.max_frequency == 1)
132
+ # It's a uniqueness constraint, and will be regenerated
133
+ @constraints_used[pc] = true
134
+ end
135
+ end
136
+
137
+ @fact_types_dumped[ft] = true
138
+
139
+ # Figure out whether any non-standard readings exist:
140
+ other_readings = ft.all_reading - [forward_reading] - [reverse_reading]
141
+ debug :mode, "--- other_readings.size now = #{other_readings.size}" if other_readings.size > 0
142
+
143
+ fact_text = other_readings.map do |reading|
144
+ expanded_reading(reading, fact_constraints, true)
145
+ end*",\n\t"
146
+ return " identified by its #{residual}" +
147
+ (fact_text != "" ? " where\n\t" + fact_text : "")
148
+ end
149
+ end
150
+
151
+ identifying_facts.each{|f| @fact_types_dumped[f] = true }
152
+ @identifying_fact_text =
153
+ identifying_facts.map{|f|
154
+ fact_readings_with_constraints(f, fact_constraints)
155
+ }.flatten*",\n\t"
156
+
157
+ " identified by #{ identifying_role_names*" and " }" +
158
+ " where\n\t"+@identifying_fact_text
159
+ end
160
+
161
+ def entity_type_banner
162
+ puts "/*\n * Entity Types\n */"
163
+ end
164
+
165
+ def entity_type_group_end
166
+ puts "\n"
167
+ end
168
+
169
+ def fact_readings(fact_type)
170
+ constrained_fact_readings = fact_readings_with_constraints(fact_type)
171
+ constrained_fact_readings*",\n\t"
172
+ end
173
+
174
+ def subtype_dump(o, supertypes, pi)
175
+ print "#{o.name} is a kind of #{ o.supertypes.map(&:name)*", " }"
176
+ if pi
177
+ print identified_by(o, pi)
178
+ end
179
+ # If there's a preferred_identifier for this subtype, identifying readings were emitted
180
+ print((pi ? "," : " where") + "\n\t" + fact_readings(o.fact_type)) if o.fact_type
181
+ puts ";\n"
182
+ end
183
+
184
+ def non_subtype_dump(o, pi)
185
+ print "#{o.name} is" + identified_by(o, pi)
186
+ print(" where\n\t"+ fact_readings(o.fact_type)) if o.fact_type
187
+ puts ";\n"
188
+ end
189
+
190
+ def fact_type_dump(fact_type, name)
191
+
192
+ @identifying_fact_text = nil
193
+ if (o = fact_type.entity_type)
194
+ print "#{o.name} is"
195
+ if !o.all_type_inheritance_by_subtype.empty?
196
+ print " a kind of #{ o.supertypes.map(&:name)*", " }"
197
+ end
198
+
199
+ # Alternate identification of objectified fact type?
200
+ primary_supertype = o.supertypes[0]
201
+ pi = fact_type.entity_type.preferred_identifier
202
+ if pi && primary_supertype && primary_supertype.preferred_identifier != pi
203
+ print identified_by(o, pi)
204
+ print ";\n"
205
+ end
206
+ end
207
+
208
+ unless @identifying_fact_text
209
+ print " where\n\t" if o
210
+ puts(fact_readings(fact_type)+";")
211
+ end
212
+ end
213
+
214
+ def fact_type_banner
215
+ puts "/*\n * Fact Types\n */"
216
+ end
217
+
218
+ def fact_type_end
219
+ puts "\n"
220
+ end
221
+
222
+ def constraint_banner
223
+ puts "/*\n * Constraints:"
224
+ puts " */"
225
+ end
226
+
227
+ def constraint_end
228
+ end
229
+
230
+ # Of the players of a set of roles, return the one that's a subclass of (or same as) all others, else nil
231
+ def roleplayer_subclass(roles)
232
+ roles[1..-1].inject(roles[0].concept){|subclass, role|
233
+ next nil unless subclass and EntityType === role.concept
234
+ role.concept.supertypes_transitive.include?(subclass) ? role.concept : nil
235
+ }
236
+ end
237
+
238
+ def dump_presence_constraint(c)
239
+ roles = c.role_sequence.all_role_ref.map{|rr| rr.role }
240
+
241
+ # REVISIT: If only one role is covered and it's mandatory >=1 constraint, use SOME/THAT form:
242
+ # each Bug SOME Tester logged THAT Bug;
243
+ players = c.role_sequence.all_role_ref.map{|rr| rr.role.concept.name}.uniq
244
+
245
+ fact_types = c.role_sequence.all_role_ref.map{|rr| rr.role.fact_type}.uniq
246
+ puts \
247
+ "each #{players.size > 1 ? "combination " : ""}#{players*", "} occurs #{c.frequency} time in\n\t"+
248
+ "#{fact_types.map{|ft| ft.default_reading([], nil)}*",\n\t"}" +
249
+ ";"
250
+
251
+ =begin
252
+ # More than one fact type involved, an external constraint.
253
+ fact_type = rr.role.fact_type
254
+ # or all facts are binary and the counterparts of the roles are.
255
+ puts "// REVISIT: " +
256
+ if (player = roleplayer_subclass(roles))
257
+ "#{player.name} must play #{c.frequency} of "
258
+ else
259
+ counterparts = roles.map{|r|
260
+ r.fact_type.all_role[r.fact_type.all_role[0] != r ? 0 : -1]
261
+ }
262
+ player = roleplayer_subclass(counterparts)
263
+ "#{c.frequency} #{player ? player.name : "UNKNOWN" } exists for each "
264
+ end +
265
+ "#{
266
+ c.role_sequence.all_role_ref.map{|rr|
267
+ "'#{rr.role.fact_type.default_reading([], nil)}'"
268
+ }*", "
269
+ }"
270
+ =end
271
+
272
+ =begin
273
+ puts \
274
+ "FOR each #{players*", "}" +
275
+ (c.role_sequence.all_role_ref.size > 1 ? " "+c.frequency+" of these holds" : "") + "\n\t"+
276
+ "#{c.role_sequence.all_role_ref.map{|rr|
277
+ role = rr.role
278
+ fact_type = role.fact_type
279
+ some_that = Array.new(fact_type.all_role.size, "some")
280
+ c.role_sequence.all_role_ref.each{|rr2|
281
+ next if rr2.role.fact_type != fact_type
282
+ some_that[fact_type.all_role.index(role)] = "that"
283
+ }
284
+ rr.role.fact_type.default_reading(some_that, nil)
285
+ }*",\n\t"}" +
286
+ ";"
287
+ =end
288
+ end
289
+
290
+ # Find the common supertype of these concepts.
291
+ # N.B. This will only work if all concepts are on the direct path to the deepest.
292
+ def common_supertype(concepts)
293
+ players_differ = false
294
+ common =
295
+ concepts[1..-1].inject(concepts[0]) do |supertype, concept|
296
+ if !supertype || concept == supertype
297
+ concept # Most common case
298
+ elsif concept.supertypes_transitive.include?(supertype)
299
+ players_differ = true
300
+ supertype
301
+ elsif supertype.supertypes_transitive.include?(concept)
302
+ players_differ = true
303
+ concept
304
+ else
305
+ return nil # No common supertype
306
+ end
307
+ end
308
+ return common, players_differ
309
+ end
310
+
311
+ def dump_set_constraint(c)
312
+ # REVISIT exclusion: every <player-list> must<?> either reading1, reading2, ...
313
+
314
+ # Each constraint involves two or more occurrences of one or more players.
315
+ # For each player, a subtype may be involved in the occurrences.
316
+ # Find the common supertype of each player.
317
+ scrs = c.all_set_comparison_roles
318
+ player_count = scrs[0].role_sequence.all_role_ref.size
319
+ role_seq_count = scrs.size
320
+
321
+ #raise "Can't verbalise constraint over many players and facts" if player_count > 1 and role_seq_count > 1
322
+
323
+ # puts "#{c.class.basename} has #{role_seq_count} scr's: #{scrs.map{|scr| "("+scr.role_sequence.all_role_ref.map{|rr| rr.role.concept.name}*", "+")"}*", "}"
324
+
325
+ players_differ = [] # Record which players are also played by subclasses
326
+ players = (0...player_count).map do |pi|
327
+ # Find the common supertype of the players of the pi'th role in each sequence
328
+ concepts = scrs.map{|r| r.role_sequence.all_role_ref[pi].role.concept }
329
+ player, players_differ[pi] = common_supertype(concepts)
330
+ raise "Role sequences of #{c.class.basename} must have concepts matching #{concept.name} in position #{pi}" unless player
331
+ player
332
+ end
333
+ #puts "#{c.class.basename} has players #{players.map{|p| p.name}*", "}"
334
+
335
+ if (SetEqualityConstraint === c)
336
+ # REVISIT: Need a proper approach to some/that and adjective disambiguation:
337
+ puts \
338
+ scrs.map{|scr|
339
+ scr.role_sequence.all_role_ref.map{|rr| rr.role.fact_type.default_reading([], nil) }*" and "
340
+ } * "\n\tif and only if\n\t" + ";"
341
+ return
342
+ end
343
+
344
+ mode = c.is_mandatory ? "exactly one" : "at most one"
345
+ puts "for each #{players.map{|p| p.name}*", "} #{mode} of these holds:\n\t" +
346
+ (scrs.map do |scr|
347
+ constrained_roles = scr.role_sequence.all_role_ref.map{|rr| rr.role }
348
+ fact_types = constrained_roles.map{|r| r.fact_type }.uniq
349
+
350
+ fact_types.map do |fact_type|
351
+ # REVISIT: future: Use "THAT" and "SOME" only when:
352
+ # - the role player occurs twice in the reading, or
353
+ # - is a subclass of the constrained concept, or
354
+ reading = fact_type.preferred_reading
355
+ expand_constrained(reading, constrained_roles, players, players_differ)
356
+ end * " and "
357
+
358
+ end*",\n\t"
359
+ )+';'
360
+ end
361
+
362
+ # Expand this reading using (in)definite articles where needed
363
+ # Handle any roles in constrained_roles specially.
364
+ def expand_constrained(reading, constrained_roles, players, players_differ)
365
+ frequency_constraints = reading.role_sequence.all_role_ref.map {|role_ref|
366
+ i = constrained_roles.index(role_ref.role)
367
+ if !i
368
+ [ "some", role_ref.role.concept.name]
369
+ elsif players_differ[i]
370
+ [ "that", players[i].name ] # Make sure to use the superclass name
371
+ else
372
+ if reading.fact_type.all_role.select{|r| r.concept == role_ref.role.concept }.size > 1
373
+ [ "that", role_ref.role.concept.name ]
374
+ else
375
+ [ "some", role_ref.role.concept.name ]
376
+ end
377
+ end
378
+ }
379
+ frequency_constraints = [] unless frequency_constraints.detect{|fc| fc[0] != "some" }
380
+
381
+ #$stderr.puts "fact_type roles (#{fact_type.all_role.map{|r| r.concept.name}*","}) default_reading '#{fact_type.preferred_reading.reading_text}' roles (#{fact_type.preferred_reading.role_sequence.all_role_ref.map{|rr| rr.role.concept.name}*","}) #{frequency_constraints.inspect}"
382
+
383
+ # REVISIT: Make sure that we refer to the constrained players by their common supertype
384
+
385
+ reading.expand(frequency_constraints, nil)
386
+ end
387
+
388
+ def dump_subset_constraint(c)
389
+ # If the role players are identical and not duplicated, we can simply say "reading1 only if reading2"
390
+ subset_roles = c.subset_role_sequence.all_role_ref.map{|rr| rr.role}
391
+ superset_roles = c.superset_role_sequence.all_role_ref.map{|rr| rr.role}
392
+
393
+ subset_players = subset_roles.map(&:concept)
394
+ superset_players = superset_roles.map(&:concept)
395
+
396
+ subset_fact_types = c.subset_role_sequence.all_role_ref.map{|rr| rr.role.fact_type }.uniq
397
+ superset_fact_types = c.superset_role_sequence.all_role_ref.map{|rr| rr.role.fact_type }.uniq
398
+
399
+ # We need to ensure that if the player of any constrained role also exists
400
+ # as the player of a role that's not a constrained role, there are different
401
+ # adjectives or other qualifiers qualifier applied to distinguish that role.
402
+ fact_type_roles = (subset_fact_types+superset_fact_types).map{|ft| ft.all_role }.flatten
403
+ non_constrained_roles = fact_type_roles - subset_roles - superset_roles
404
+ if (r = non_constrained_roles.detect{|r| (subset_roles+superset_roles).include?(r) })
405
+ # REVISIT: Find a way to deal with this problem, should it arise.
406
+
407
+ # It would help, but not entirely fix it, to use SOME/THAT to identify the constrained roles.
408
+ # See ServiceDirector's DataStore<->Client fact types for example
409
+ # Use SOME on the subset, THAT on the superset.
410
+ raise "Critical ambiguity, #{r.concept.name} occurs both constrained and unconstrained in #{c.name}"
411
+ end
412
+
413
+ puts \
414
+ "#{subset_fact_types.map{|ft| ft.default_reading([], nil)}*" and "}" +
415
+ "\n\tonly if " +
416
+ "#{superset_fact_types.map{|ft| ft.default_reading([], nil)}*" and "}" +
417
+ ";"
418
+ end
419
+
420
+ def dump_ring_constraint(c)
421
+ # At present, no ring constraint can be missed to be handled in this pass
422
+ puts "// #{c.ring_type} ring over #{c.role.fact_type.default_reading([], nil)}"
423
+ end
424
+
425
+ def constraint_dump(c)
426
+ case c
427
+ when PresenceConstraint
428
+ dump_presence_constraint(c)
429
+ when RingConstraint
430
+ dump_ring_constraint(c)
431
+ when SetComparisonConstraint # includes SetExclusionConstraint, SetEqualityConstraint
432
+ dump_set_constraint(c)
433
+ when SubsetConstraint
434
+ dump_subset_constraint(c)
435
+ else
436
+ "#{c.class.basename} #{c.name}: unhandled constraint type"
437
+ end
438
+ end
439
+ end
440
+ end
441
+ end