activefacts 0.8.13 → 0.8.15

Sign up to get free protection for your applications and to get access to all the features.
@@ -264,7 +264,7 @@ module ActiveFacts
264
264
  {
265
265
  def ast
266
266
  raise "Not implemented: AST for '#{aggregate.text_value} of #{term.text_value}'"
267
- # This returns just the role with the nested clauses, which doesn;t even work:
267
+ # This returns just the role with the nested clauses, which doesn't even work:
268
268
  term.ast(
269
269
  nil, # No quantifier
270
270
  nil, # No function call
@@ -0,0 +1,273 @@
1
+ #
2
+ # ActiveFacts Generators.
3
+ # Generate Ruby code for Data Mapper from an ActiveFacts vocabulary.
4
+ #
5
+ # A testing strategy:
6
+ # Generate and load a set of models
7
+ # call DataMapper::finalize to check they're consistent
8
+ # call DataMapper::Spec.cleanup_models to delete them again
9
+ #
10
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
11
+ #
12
+ require 'activefacts/vocabulary'
13
+ require 'activefacts/persistence'
14
+ require 'activefacts/generate/oo'
15
+
16
+ module ActiveFacts
17
+ module Generate
18
+ class DM < OO #:nodoc:
19
+ # Generate SQL for DataMapper for an ActiveFacts vocabulary.
20
+ # Invoke as
21
+ # afgen --dm[=options] <file>.cql
22
+ # Options:
23
+ # dir=<mixins directory>
24
+ # Example:
25
+ # afgen --dm=dir=app/mixins MyApp.cql
26
+ include Persistence
27
+
28
+ def initialize(vocabulary, *options)
29
+ @vocabulary = vocabulary
30
+ @vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
31
+ @mixins = options.grep(/^dir=/)[-1]
32
+ @mixins && @mixins.sub!(/^dir=/,'')
33
+ end
34
+
35
+ def puts s
36
+ @out.puts s
37
+ end
38
+
39
+ def model_file(name)
40
+ @mixins+'/'+name.gsub(/\s/,'')+'.rb'
41
+ end
42
+
43
+ def class_name(name)
44
+ name.gsub(/\s/,'')
45
+ end
46
+
47
+ def column_name(column)
48
+ column.name('_').snakecase
49
+ end
50
+
51
+ def symbol_name(name)
52
+ name.gsub(/\s/,'_').snakecase
53
+ end
54
+
55
+ def new_output(name)
56
+ return unless @mixins
57
+ @out.flush
58
+ @out = File.open(model_file(name), "w")
59
+ puts "require 'datamapper'\n\n"
60
+ end
61
+
62
+ def key_fields(ref, reverse = false)
63
+ # Compute and return child_key and parent_key if necessary
64
+ fk = ref.from.foreign_keys.detect{|k| k.reference == ref}
65
+ child_key = fk.from_columns.map{|c| column_name(c)}
66
+ parent_key = fk.to_columns.map{|c| column_name(c)}
67
+ if child_key != parent_key
68
+ c, p = *(reverse ? ['parent', 'child'] : ['child', 'parent'])
69
+ ", :#{c}_key => [:#{child_key*', :'}], :#{p}_key => [:#{parent_key*', :'}]"
70
+ else
71
+ ''
72
+ end
73
+ end
74
+
75
+ public
76
+
77
+ def generate(out = $>) #:nodoc:
78
+ @out = out
79
+
80
+ # Calculate the relational absorption:
81
+ tables = @vocabulary.tables
82
+
83
+ # Figure out which ObjectType will be models (tables and their subtypes)
84
+ models =
85
+ @vocabulary.all_object_type.sort_by{|o| o.name}.select do |o|
86
+ next false if o.name =~ /_?ImplicitBooleanValueType/
87
+ o.is_table || (o.absorbed_via && o.absorbed_via.role_type == :supertype)
88
+ end
89
+ is_model = models.inject({}) { |h, m| h[m] = true; h }
90
+
91
+ puts "require 'dm-core'"
92
+ puts "require 'dm-constraints'"
93
+ puts "\n"
94
+
95
+ # Dump tables until all done, subtypes before supertypes:
96
+ until models.empty?
97
+ # Choose another object type that we can dump now:
98
+ o = models.detect do |o|
99
+ next true if o.is_table
100
+ next true if a = o.absorbed_via and a.role_type == :supertype and supertype = a.from and !models.include?(supertype)
101
+ false
102
+ end
103
+ models.delete(o)
104
+
105
+ supertype = (a = o.absorbed_via and a.role_type == :supertype) ? supertype = a.from : nil
106
+ if o.is_a?(ActiveFacts::Metamodel::EntityType)
107
+ if secondary_supertypes = o.supertypes-[supertype] and
108
+ secondary_supertypes.size > 0 and
109
+ secondary_supertypes.detect do |sst|
110
+ sst_ref_facts = sst.preferred_identifier.role_sequence.all_role_ref.map{|rr| rr.role.fact_type}.uniq
111
+ non_identifying_inheritable_references =
112
+ sst.references_from.reject do |ref|
113
+ sst_ref_facts.include?(ref.fact_type)
114
+ end
115
+ non_identifying_inheritable_references.size > 0
116
+ end
117
+ raise "Cannot map classes like #{o.name} with roles inherited from external supertypes (#{secondary_supertypes.map{|t|t.name}*", "})"
118
+ end
119
+ pi = o.preferred_identifier
120
+ identifying_role_refs = pi.role_sequence.all_role_ref.sort_by{|role_ref| role_ref.ordinal}
121
+ identifying_facts = ([o.fact_type]+identifying_role_refs.map{|rr| rr.role.fact_type }).compact.uniq
122
+ else
123
+ identifying_facts = []
124
+ end
125
+
126
+ # REVISIT: STI fails where the base class is absorbed into another table, like Incident in Insurance for example.
127
+ # In this case you get the subtype fields absorbed and should not get an STI model.
128
+
129
+ puts "class #{class_name(o.name)}#{supertype ? " < #{class_name(supertype.name)}" : ''}"
130
+ puts " include DataMapper::Resource\n\n" unless supertype
131
+
132
+ columns = o.columns
133
+ o.references_from.each do |ref|
134
+ # A (set of) columns
135
+ if !columns
136
+ # absorbed subtypes didn't have columns populated
137
+ columns = o.all_columns({})
138
+ end
139
+
140
+ next if [:subtype, :supertype].include?(ref.role_type)
141
+ # debugger if ref_columns.detect{|c| [:subtype, :supertype].include?(c.references[0].role_type)}
142
+ ref_columns = columns.select{|c| c.references[0] == ref }
143
+ # puts " \# #{ref.reading}:"
144
+ ref_columns.each do |column|
145
+ type, params, constraints = column.type
146
+ length = params[:length]
147
+ length &&= length.to_i
148
+ scale = params[:scale]
149
+ scale &&= scale.to_i
150
+ type, length = normalise_type(type, length)
151
+ key = identifying_facts.include?(column.references[0].fact_type) ||
152
+ (identifying_facts.empty? && ref.is_self_value)
153
+ cname = column_name(column)
154
+ required = column.is_mandatory && !key ? ", :required => true" : "" # Key fields are implicitly required
155
+ if type == 'Serial'
156
+ if !key || o.preferred_identifier.role_sequence.all_role_ref.size != 1
157
+ type = 'Integer'
158
+ else
159
+ key = false # This is implicit
160
+ end
161
+ end
162
+ $stderr.puts "Warning: non-mandatory key field #{o.name}.#{column.name} is forced to mandatory" if !column.is_mandatory && key
163
+ puts " property :#{column_name(column)}, #{type}#{length ? ", :length => "+length.to_s : ''}#{required}#{key ? ', :key => true' : ''}\t\# #{column.comment}"
164
+ end
165
+
166
+ if is_model[ref.to]
167
+ # An association
168
+ reverse = false
169
+ association_type =
170
+ case ref.role_type
171
+ when :one_one
172
+ reverse = true
173
+ "has 1,"
174
+ when :one_many, :many_one
175
+ "belongs_to"
176
+ when :supertype
177
+ next
178
+ when :subtype
179
+ next
180
+ else
181
+ raise "role type #{ref.role_type} not handled"
182
+ end
183
+
184
+ association_name = (ref.to_names*'_')
185
+ model_name = association_name != ref.to.name ? model_name = ", '#{class_name(ref.to.name)}'" : ''
186
+ comment = o.fact_type ? "#{association_name} is involved in #{o.name}" : ref.reading
187
+ keys = key_fields(ref, reverse)
188
+ puts " #{association_type} :#{association_name.downcase}#{model_name}#{keys}\t\# #{comment}"
189
+ end
190
+ end
191
+
192
+ # Emit the "has n," associations
193
+ # REVISIT: Need to use ActiveSupport to pluralise these names, or disable inflexion somehow.
194
+ o.references_to.each do |ref|
195
+ next unless is_model[ref.from]
196
+ constraint = ''
197
+ association_type =
198
+ case ref.role_type
199
+ when :one_one
200
+ "has 1,"
201
+ when :many_one, :one_many
202
+ constraint = ', :constraint => :destroy' # REVISIT: Check mandatory, and use nullify?
203
+ "has n,"
204
+ else
205
+ next
206
+ end
207
+ prr = ref.fact_type.preferred_reading.role_sequence.all_role_ref.detect{|rr| rr.role == ref.to_role}
208
+ association_name = (ref.from_names*'_')
209
+ if prr && (prr.role.role_name || prr.leading_adjective || prr.trailing_adjective)
210
+ association_name += "_as_"+symbol_name(ref.to_names*'_')
211
+ end
212
+ model_name = association_name != ref.from.name ? model_name = ", '#{class_name(ref.from.name)}'" : ''
213
+ comment = o.is_a?(ActiveFacts::Metamodel::EntityType) && o.fact_type ? "#{association_name} is involved in #{o.name}" : ref.reading
214
+ keys = key_fields(ref)
215
+
216
+ puts " #{association_type} :#{association_name.downcase}#{model_name}#{keys}\t\# #{comment}"
217
+ end
218
+ puts "end\n\n"
219
+ end
220
+ end
221
+
222
+
223
+ # Return DataMapper type and (modified?) length for the passed base type
224
+ def normalise_type(type, length)
225
+ dm_type = case type
226
+ when /^Auto ?Counter$/
227
+ 'Serial'
228
+
229
+ when /^Unsigned ?Integer$/,
230
+ /^Signed ?Integer$/,
231
+ /^Unsigned ?Small ?Integer$/,
232
+ /^Signed ?Small ?Integer$/,
233
+ /^Unsigned ?Tiny ?Integer$/
234
+ length = nil
235
+ 'Integer'
236
+
237
+ when /^Decimal$/
238
+ 'Decimal'
239
+
240
+ when /^Fixed ?Length ?Text$/, /^Char$/
241
+ 'String'
242
+ when /^Variable ?Length ?Text$/, /^String$/
243
+ 'String'
244
+ when /^Large ?Length ?Text$/, /^Text$/
245
+ 'Text'
246
+
247
+ when /^Date ?And ?Time$/, /^Date ?Time$/
248
+ 'DateTime'
249
+ when /^Date$/
250
+ 'DateTime'
251
+ when /^Time$/
252
+ 'DateTime'
253
+ when /^Auto ?Time ?Stamp$/
254
+ 'DateTime'
255
+
256
+ when /^Money$/
257
+ 'Decimal'
258
+ when /^Picture ?Raw ?Data$/, /^Image$/
259
+ 'String'
260
+ when /^Variable ?Length ?Raw ?Data$/, /^Blob$/
261
+ 'String'
262
+ when /^BIT$/
263
+ 'Boolean'
264
+ else
265
+ # raise "DataMapper type unknown for standard type #{type}"
266
+ type
267
+ end
268
+ [dm_type, length]
269
+ end
270
+
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,45 @@
1
+ #
2
+ # ActiveFacts Generators.
3
+ # Provides help for afgen - from afgen --help
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ require 'activefacts/persistence'
8
+
9
+ module ActiveFacts
10
+ module Generate
11
+ # Generate nothing from an ActiveFacts vocabulary. This is useful to check the file can be read ok.
12
+ # Invoke as
13
+ # afgen --null <file>.cql
14
+ class HELP
15
+ private
16
+ def initialize(vocabulary, *options)
17
+ puts %Q{
18
+ Usage: afgen --generator[=options] file.inp[=options]
19
+ options are comma-separated lists. Use =help to get more information.
20
+
21
+ Available generators are:
22
+ #{$:.map{|path|
23
+ Dir[path+"/activefacts/generate/**.rb"].map{|p|
24
+ p.sub(%r{.*/}, '').sub(/\.rb/,'')
25
+ }
26
+ }.flatten.uniq.sort.join("\n\t")
27
+ }
28
+
29
+ inp is the name of a file input handler. Available input handlers are:
30
+ #{$:.map{|path|
31
+ Dir[path+"/activefacts/input/**.rb"].map{|p|
32
+ p.sub(%r{.*/}, '').sub(/\.rb/,'')
33
+ }
34
+ }.flatten.uniq.sort.join("\n\t")
35
+ }
36
+ }
37
+ end
38
+
39
+ public
40
+ def generate(out = $>)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
@@ -0,0 +1,209 @@
1
+ #
2
+ # ActiveFacts Generators.
3
+ #
4
+ # Generate a glossary in HTML
5
+ #
6
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
7
+ #
8
+ require 'activefacts/api'
9
+
10
+ module ActiveFacts
11
+ module Generate #:nodoc:
12
+ class HTML #:nodoc:
13
+ class GLOSSARY #:nodoc:
14
+ # Base class for generators of object-oriented class libraries for an ActiveFacts vocabulary.
15
+ def initialize(vocabulary, *options)
16
+ @vocabulary = vocabulary
17
+ @vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
18
+ options.each{|option| set_option(option) }
19
+ end
20
+
21
+ def set_option(option)
22
+ end
23
+
24
+ def puts(*a)
25
+ @out.puts *a
26
+ end
27
+
28
+ def print(*a)
29
+ @out.print *a
30
+ end
31
+
32
+ def generate(out = $>)
33
+ @out = out
34
+ vocabulary_start
35
+
36
+ object_types_dump()
37
+
38
+ vocabulary_end
39
+ end
40
+
41
+ def vocabulary_start
42
+ puts "<link rel='stylesheet' href='css/orm2.css' media='screen' type='text/css'/>"
43
+ puts "<h1>#{@vocabulary.name}</h1>"
44
+ puts "<dl>"
45
+ end
46
+
47
+ def vocabulary_end
48
+ puts "</dl>"
49
+ end
50
+
51
+ def object_types_dump
52
+ @vocabulary.
53
+ all_object_type.
54
+ sort_by{|o| o.name.gsub(/ /,'').downcase}.
55
+ each do |o|
56
+ case o
57
+ when ActiveFacts::Metamodel::TypeInheritance
58
+ nil
59
+ when ActiveFacts::Metamodel::ValueType
60
+ value_type_dump(o)
61
+ else
62
+ if o.fact_type
63
+ objectified_fact_type_dump(o)
64
+ else
65
+ entity_type_dump(o)
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ def element(text, attrs, tag = 'span')
72
+ "<#{tag}#{attrs.empty? ? '' : attrs.map{|k,v| " #{k}='#{v}'"}*''}>#{text}</#{tag}>"
73
+ end
74
+
75
+ # A definition of a term
76
+ def termdef(name)
77
+ element(name, {:name => name, :class => 'object_type'}, 'a')
78
+ end
79
+
80
+ # A reference to a defined term (excluding role adjectives)
81
+ def termref(name, role_name = nil)
82
+ role_name ||= name
83
+ element(role_name, {:href=>'#'+name, :class=>:object_type}, 'a')
84
+ end
85
+
86
+ # Text that should appear as part of a term (including role adjectives)
87
+ def term(name)
88
+ element(name, :class=>:object_type)
89
+ end
90
+
91
+ def value_type_dump(o)
92
+ return if o.all_role.size == 0 # Skip value types that are only used as supertypes
93
+ puts " <dt>" +
94
+ "Value Type: #{termdef(o.name)}" +
95
+ (o.supertype ? " (written as #{termref(o.supertype.name)})" : "") +
96
+ "</dt>"
97
+
98
+ puts " <dd>"
99
+ relevant_facts_and_constraints(o)
100
+ puts " </dd>"
101
+ end
102
+
103
+ def relevant_facts_and_constraints(o)
104
+ puts(
105
+ o.
106
+ all_role.
107
+ map{|r| r.fact_type}.
108
+ uniq.
109
+ reject{|ft| ft.is_a?(ActiveFacts::Metamodel::TypeInheritance) || ft.is_a?(ActiveFacts::Metamodel::ImplicitFactType) }.
110
+ map { |ft| " #{fact_type_with_constraints(ft, o)}</br>" }.
111
+ sort * "\n"
112
+ )
113
+ end
114
+
115
+ def role_ref rr, freq_con, l_adj, name, t_adj, role_name_def, literal
116
+ term_parts = [l_adj, termref(name), t_adj].compact
117
+ [
118
+ freq_con ? element(freq_con, :class=>:keyword) : nil,
119
+ term_parts.size > 1 ? term([l_adj, termref(name), t_adj].compact*' ') : term_parts[0],
120
+ role_name_def,
121
+ literal
122
+ ]
123
+ end
124
+
125
+ def expand_reading(r)
126
+ element(
127
+ r.expand do |*a|
128
+ role_ref(*a)
129
+ end,
130
+ {:class => 'copula'}
131
+ )
132
+ end
133
+
134
+ def fact_type(ft, wrt = nil)
135
+ role = ft.all_role.detect{|r| r.object_type == wrt}
136
+ preferred_reading = ft.reading_preferably_starting_with_role(role)
137
+ alternate_readings = ft.all_reading.reject{|r| r == preferred_reading}
138
+ expand_reading(preferred_reading) +
139
+ (alternate_readings.size > 0 ?
140
+ ' (alternatively, ' +
141
+ alternate_readings.map { |r| expand_reading(r)}*', ' +
142
+ ')' : '')
143
+ end
144
+
145
+ def fact_type_with_constraints(ft, wrt = nil)
146
+ fact_type(ft, wrt) +
147
+ "<br/>\n<ul>\n" +
148
+ fact_type_constraints(ft) +
149
+ "</ul>"
150
+ end
151
+
152
+ def fact_type_constraints(ft)
153
+ ft.internal_presence_constraints.map do |pc|
154
+ residual_role = ft.all_role.detect{|r| !pc.role_sequence.all_role_ref.detect{|rr| rr.role == r}}
155
+ next nil unless residual_role
156
+ reading = ft.all_reading.detect{|reading|
157
+ reading.role_sequence.all_role_ref_in_order[reading.role_numbers[-1]].role == residual_role
158
+ }
159
+ next nil unless reading
160
+ element(
161
+ reading.expand_with_final_presence_constraint { |*a| role_ref(*a) },
162
+ {:class => 'copula'}
163
+ )+"<br/>\n"
164
+ end.compact*''
165
+ end
166
+
167
+ def objectified_fact_type_dump(o)
168
+ puts " <dt>" +
169
+ "Entity Type: #{termdef(o.name)}" +
170
+ " (objectification of #{fact_type(o.fact_type)})" +
171
+ "</dt>"
172
+ # REVISIT: Handle separate identification
173
+
174
+ puts " <dd>"
175
+ puts fact_type_constraints(o.fact_type)
176
+ o.fact_type.all_role_in_order.each do |r|
177
+ n = r.object_type.name
178
+ puts "#{termref(o.name)} involves exactly one #{termref(r.role_name || n, n)}<br/>"
179
+ end
180
+ relevant_facts_and_constraints(o)
181
+ puts " </dd>"
182
+ end
183
+
184
+ def entity_type_dump(o)
185
+ pi = o.preferred_identifier
186
+ supers = o.supertypes
187
+ if (supers.size > 0) # Ignore identification by a supertype:
188
+ pi = nil if pi && pi.role_sequence.all_role_ref.detect{|rr| rr.role.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance) }
189
+ end
190
+
191
+ puts " <dt>" +
192
+ "Entity Type: #{termdef(o.name)}" +
193
+ " (" +
194
+ [
195
+ (supers.size > 0 ? "Subtype of #{supers.map{|s| s.name}*', '})" : nil),
196
+ (pi ? "identified by "+pi.role_sequence.describe : nil)
197
+ ].compact*', '
198
+ ")" +
199
+ "</dt>"
200
+
201
+ puts " <dd>"
202
+ relevant_facts_and_constraints(o)
203
+ puts " </dd>"
204
+ end
205
+
206
+ end
207
+ end
208
+ end
209
+ end