activefacts 0.8.13 → 0.8.15

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.
@@ -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