activefacts-generators 1.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +10 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +30 -0
  8. data/Rakefile +6 -0
  9. data/activefacts-generators.gemspec +26 -0
  10. data/lib/activefacts/dependency_analyser.rb +182 -0
  11. data/lib/activefacts/generators/absorption.rb +71 -0
  12. data/lib/activefacts/generators/composition.rb +119 -0
  13. data/lib/activefacts/generators/cql.rb +715 -0
  14. data/lib/activefacts/generators/diagrams/json.rb +340 -0
  15. data/lib/activefacts/generators/help.rb +64 -0
  16. data/lib/activefacts/generators/helpers/inject.rb +16 -0
  17. data/lib/activefacts/generators/helpers/oo.rb +162 -0
  18. data/lib/activefacts/generators/helpers/ordered.rb +605 -0
  19. data/lib/activefacts/generators/helpers/rails.rb +57 -0
  20. data/lib/activefacts/generators/html/glossary.rb +462 -0
  21. data/lib/activefacts/generators/metadata/json.rb +204 -0
  22. data/lib/activefacts/generators/null.rb +32 -0
  23. data/lib/activefacts/generators/rails/models.rb +247 -0
  24. data/lib/activefacts/generators/rails/schema.rb +217 -0
  25. data/lib/activefacts/generators/ruby.rb +134 -0
  26. data/lib/activefacts/generators/sql/mysql.rb +281 -0
  27. data/lib/activefacts/generators/sql/server.rb +274 -0
  28. data/lib/activefacts/generators/stats.rb +70 -0
  29. data/lib/activefacts/generators/text.rb +29 -0
  30. data/lib/activefacts/generators/traits/datavault.rb +241 -0
  31. data/lib/activefacts/generators/traits/oo.rb +73 -0
  32. data/lib/activefacts/generators/traits/ordered.rb +33 -0
  33. data/lib/activefacts/generators/traits/ruby.rb +210 -0
  34. data/lib/activefacts/generators/transform/datavault.rb +303 -0
  35. data/lib/activefacts/generators/transform/surrogate.rb +215 -0
  36. data/lib/activefacts/registry.rb +11 -0
  37. metadata +176 -0
@@ -0,0 +1,204 @@
1
+ #
2
+ # ActiveFacts Generators.
3
+ #
4
+ # Generate metadata in JSON
5
+ #
6
+ # Copyright (c) 2013 Clifford Heath. Read the LICENSE file.
7
+ #
8
+ require 'activefacts/api'
9
+ require 'activefacts/rmap'
10
+ require 'json'
11
+ require 'activefacts/registry'
12
+
13
+ module ActiveFacts
14
+ module Generators #:nodoc:
15
+ class Metadata #:nodoc:
16
+ class JSON #:nodoc:
17
+ def initialize(vocabulary, *options)
18
+ @vocabulary = vocabulary
19
+ @vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
20
+ options.each{|option| set_option(option) }
21
+ end
22
+
23
+ def set_option(option)
24
+ end
25
+
26
+ def generate(out = $>)
27
+ @metadata = {"types" => {}}
28
+
29
+ object_types_dump
30
+
31
+ out.puts ::JSON.pretty_generate(@metadata)
32
+ end
33
+
34
+ # Store the metadata for all types into the types section of the @metadata hash
35
+ def object_types_dump
36
+ types = @metadata["types"]
37
+
38
+ # Compute the relational mapping if not already done:
39
+ @tables ||= @vocabulary.tables
40
+
41
+ @vocabulary.all_object_type.
42
+ sort_by{|c| c.name}.each do |o|
43
+ object_type = o.as_json_metadata
44
+
45
+ types[o.name] = object_type if object_type
46
+ end
47
+ end
48
+
49
+ end
50
+ end
51
+ end
52
+
53
+ module Metamodel
54
+ class ObjectType
55
+ def as_json_metadata
56
+ # Using proc avoids polluting the object's namespace with these little methods
57
+ verbalise_role = proc do |role, plural|
58
+ fc = Array.new(role.fact_type.all_role.size, plural ? 'some' : 'one')
59
+ reading = role.fact_type.reading_preferably_starting_with_role(role)
60
+ fc.reverse! unless reading.role_sequence.all_role_ref.to_a[0].role == role
61
+ fc[reading.role_sequence.all_role_ref_in_order.to_a.index{|rr| rr.role == role}] = 'this'
62
+ reading.expand(fc, false)
63
+ end
64
+
65
+ titlize_words = proc do |phrase|
66
+ phrase && phrase.split(/\s+/).map{|w| w.sub(/^[a-z]/) {|i| i.upcase}}*' '
67
+ end
68
+
69
+ role_name = proc do |role|
70
+ if role.role_name
71
+ role.role_name
72
+ else
73
+ ref = role.preferred_reference
74
+ [ titlize_words.call(ref.leading_adjective), role.object_type.name, titlize_words.call(ref.trailing_adjective)].compact*' '
75
+ end
76
+ end
77
+
78
+ return nil if name == '_ImplicitBooleanValueType'
79
+
80
+ object_type = {}
81
+ object_type["is_main"] = is_table
82
+ object_type["id"] = concept.guid.to_s
83
+ functions = object_type["functions"] = []
84
+
85
+ if is_a?(ActiveFacts::Metamodel::EntityType)
86
+
87
+ # Don't emit a binary objectified fact type that plays no roles (except in implicit fact types:
88
+ if fact_type and fact_type.all_role.size == 2 and all_role.size == 2
89
+ return nil
90
+ end
91
+
92
+ # Export the supertypes
93
+ (supertypes_transitive-[self]).sort_by{|t| t.name}.each do |supertype|
94
+ functions <<
95
+ {
96
+ "title" => "as #{supertype.name}",
97
+ "type" => "#{supertype.name}"
98
+ }
99
+ end
100
+
101
+ # Export the subtypes
102
+ (subtypes_transitive-[self]).sort_by{|t| t.name}.each do |subtype|
103
+ functions <<
104
+ {
105
+ "title" => "as #{subtype.name}",
106
+ "type" => "#{subtype.name}"
107
+ }
108
+ end
109
+
110
+ # If an objectified fact type, export the fact type's roles
111
+ if fact_type
112
+ fact_type.preferred_reading.role_sequence.all_role_ref_in_order.map(&:role).each do |role|
113
+ functions <<
114
+ {
115
+ "title" => "involving #{role_name.call(role)}",
116
+ "type" => "#{role.object_type.name}",
117
+ "where" => verbalise_role.call(role, true) # REVISIT: Need plural setting here!
118
+ }
119
+ end
120
+ end
121
+ end
122
+
123
+ # Now export the ordinary roles. Get a sorted list first:
124
+ roles = all_role.reject do |role|
125
+ # supertype and subtype roles get handled separately
126
+ role.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance) ||
127
+ role.fact_type.is_a?(ActiveFacts::Metamodel::LinkFactType)
128
+ end.sort_by do |role|
129
+ # Where this object type plays two roles in the same fact type,
130
+ # we order them by the position of that role in the preferred reading:
131
+ [role.fact_type.default_reading, role.fact_type.preferred_reading.role_sequence.all_role_ref_in_order.map(&:role).index(role)]
132
+ end
133
+
134
+ # For binary fact types, collect the count of the times the unadorned counterpart role name occurs, so we can adorn it
135
+ plural_counterpart_counts = roles.inject(Hash.new{0}) do |h, role|
136
+ next h unless role.fact_type.all_role.size == 2
137
+ uc = role.all_role_ref.detect do |rr|
138
+ rs = rr.role_sequence
139
+ next false if rs.all_role_ref.size != 1 # Looking for a UC over just this one role
140
+ rs.all_presence_constraint.detect do |pc|
141
+ next false unless pc.max_frequency == 1 # It's a uniqueness constraint
142
+ true
143
+ end
144
+ end
145
+ next h if uc # Not a plural role
146
+
147
+ counterpart_role = (role.fact_type.all_role.to_a - [role])[0]
148
+ h[role_name.call(counterpart_role)] += 1
149
+ h
150
+ end
151
+
152
+ roles.each do |role|
153
+ type_name = nil
154
+ counterpart_name = nil
155
+
156
+ if role.fact_type.entity_type and # Role is in an objectified fact type
157
+ # For binary objectified fact types, we traverse directly to the other role, not just to the objectification
158
+ !(role.fact_type.entity_type.all_role.size == 2 and role.fact_type.all_role.size == 2)
159
+
160
+ type_name = role.fact_type.entity_type.name
161
+ counterpart_name = type_name # If self plays more than one role in OFT, need to construct a role name
162
+ plural = true
163
+ elsif role.fact_type.all_role.size == 1
164
+ # Handle unary roles
165
+ type_name = 'boolean'
166
+ counterpart_name = role.fact_type.default_reading
167
+ plural = false
168
+ else
169
+ # Handle binary roles
170
+ counterpart_role = (role.fact_type.all_role.to_a - [role])[0]
171
+ type_name = counterpart_role.object_type.name
172
+ counterpart_name = role_name.call(counterpart_role)
173
+ # Figure out whether the counterpart is plural (say "all ..." if so)
174
+ uc = role.all_role_ref.detect do |rr|
175
+ rs = rr.role_sequence
176
+ next false if rs.all_role_ref.size != 1 # Looking for a UC over just this one role
177
+ rs.all_presence_constraint.detect do |pc|
178
+ next false unless pc.max_frequency == 1 # It's a uniqueness constraint
179
+ true
180
+ end
181
+ end
182
+ plural = !uc
183
+ if plural_counterpart_counts[counterpart_name] > 1
184
+ counterpart_name += " as " + role_name.call(role)
185
+ end
186
+ end
187
+
188
+ node = {
189
+ "title" => "#{plural ? 'all ' : ''}#{counterpart_name}",
190
+ "type" => "#{type_name}",
191
+ "where" => verbalise_role.call(role, plural),
192
+ "role_id" => role.concept.guid.to_s
193
+ }
194
+ node["is_list"] = true if plural
195
+ functions << node
196
+
197
+ end
198
+ functions.size > 0 ? object_type : nil
199
+ end
200
+ end
201
+ end
202
+ end
203
+
204
+ ActiveFacts::Registry.generator('metadata/json', ActiveFacts::Generators::Metadata::JSON)
@@ -0,0 +1,32 @@
1
+ #
2
+ # ActiveFacts Generators.
3
+ # Generate *no* output for ActiveFacts vocabularies; i.e. just a stub
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ require 'activefacts/rmap'
8
+
9
+ module ActiveFacts
10
+ module Generators
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 NULL
15
+ private
16
+ def initialize(vocabulary, *options)
17
+ @vocabulary = vocabulary
18
+ @vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
19
+ @tables = options.include? "tables"
20
+ @columns = options.include? "columns"
21
+ @indices = options.include? "indices"
22
+ end
23
+
24
+ public
25
+ def generate(out = $>)
26
+ @vocabulary.tables if @tables || @columns || @indices
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ ActiveFacts::Registry.generator('null', ActiveFacts::Generators::NULL)
@@ -0,0 +1,247 @@
1
+ #
2
+ # ActiveFacts Generators.
3
+ # Generate models for Rails from an ActiveFacts vocabulary.
4
+ #
5
+ # Models should normally be generated into "app/models/auto",
6
+ # then extend(ed) into your real models.
7
+ #
8
+ # Copyright (c) 2013 Clifford Heath. Read the LICENSE file.
9
+ #
10
+ require 'activefacts/metamodel'
11
+ require 'activefacts/rmap'
12
+ #require 'activefacts/generators/helpers/rails'
13
+ require 'activefacts/generators/traits/rails'
14
+ require 'active_support'
15
+ require 'activefacts/registry'
16
+
17
+ module ActiveFacts
18
+ module Generators
19
+ module Rails
20
+ # Generate Rails models for the vocabulary
21
+ # Invoke as
22
+ # afgen --rails/schema[=options] <file>.cql
23
+ class Models
24
+
25
+ HEADER = "# Auto-generated from CQL, edits will be lost"
26
+
27
+ private
28
+
29
+ def initialize(vocabulary, *options)
30
+ @vocabulary = vocabulary
31
+ @vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
32
+ help if options.include? "help"
33
+ options.delete_if { |option| @output = $1 if option =~ /^output=(.*)/ }
34
+ @concern = nil
35
+ options.delete_if { |option| @concern = $1 if option =~ /^concern=(.*)/ }
36
+ @validations = true
37
+ options.delete_if { |option| @validations = eval($1) if option =~ /^validation=(.*)/ }
38
+ end
39
+
40
+ def help
41
+ @helping = true
42
+ warn %Q{Options for --rails/schema:
43
+ output=dir Overwrite model files into this output directory
44
+ concern=name Namespace for the concerns
45
+ validation=false Disable generation of validations
46
+ }
47
+ end
48
+
49
+ def warn *a
50
+ $stderr.puts *a
51
+ end
52
+
53
+ def puts s
54
+ @out.puts s
55
+ end
56
+
57
+ public
58
+ def generate(out = $>) #:nodoc:
59
+ return if @helping
60
+ @out = out
61
+ list_extant_files if @output
62
+
63
+ # Populate all foreignkeys first:
64
+ @vocabulary.tables.each { |table| table.foreign_keys }
65
+ ok = true
66
+ @vocabulary.tables.each do |table|
67
+ ok &= generate_table(table)
68
+ end
69
+ $stderr.puts "\# #{@vocabulary.name} generated with errors" unless ok
70
+ delete_old_generated_files if @output
71
+ ok
72
+ end
73
+
74
+ def list_extant_files
75
+ @preexisting_files = Dir[@output+'/*.rb']
76
+ end
77
+
78
+ def delete_old_generated_files
79
+ remaining = []
80
+ cleaned = 0
81
+ @preexisting_files.each do |pathname|
82
+ if generated_file_exists(pathname) == true
83
+ File.unlink(pathname)
84
+ cleaned += 1
85
+ else
86
+ remaining << pathname
87
+ end
88
+ end
89
+ $stderr.puts "Cleaned up #{cleaned} old generated files" if @preexisting_files.size > 0
90
+ $stderr.puts "Remaining non-generated files:\n\t#{remaining*"\n\t"}" if remaining.size > 0
91
+ end
92
+
93
+ def generated_file_exists pathname
94
+ File.open(pathname, 'r') do |existing|
95
+ first_lines = existing.read(1024) # Make it possible to pass over a magic charset comment
96
+ if first_lines.length == 0 or first_lines =~ %r{^#{HEADER}}
97
+ return true
98
+ end
99
+ end
100
+ return false # File exists, but is not generated
101
+ rescue Errno::ENOENT
102
+ return nil # File does not exist
103
+ end
104
+
105
+ def create_if_ok filename
106
+ # Create a file in the output directory, being careful not to overwrite carelessly
107
+ if @output
108
+ pathname = (@output+'/'+filename).gsub(%r{//+}, '/')
109
+ @preexisting_files.reject!{|f| f == pathname } # Don't clean up this file
110
+ if generated_file_exists(pathname) == false
111
+ $stderr.puts "not overwriting non-generated file #{pathname}"
112
+ @individual_file = nil
113
+ return
114
+ end
115
+ @individual_file = @out = File.open(pathname, 'w')
116
+ puts "#{HEADER}"
117
+ end
118
+ true
119
+ end
120
+
121
+ def to_associations table
122
+ # belongs_to Associations
123
+ table.foreign_keys.map do |fk|
124
+ association_name = fk.rails_from_association_name
125
+
126
+ if association_name != fk.to.rails_singular_name
127
+ # A different class_name is implied, emit an explicit one:
128
+ class_name = ", :class_name => '#{fk.to.rails_class_name}'"
129
+ end
130
+ foreign_key = ", :foreign_key => :#{fk.from_columns[0].rails_name}"
131
+ if foreign_key == fk.to.rails_singular_name+'_id'
132
+ # See lib/active_record/reflection.rb, method #derive_foreign_key
133
+ foreign_key = ''
134
+ end
135
+
136
+ %Q{
137
+ \# #{fk.verbalised_path}
138
+ belongs_to :#{association_name}#{class_name}#{foreign_key}}
139
+ end
140
+ end
141
+
142
+ def from_associations table
143
+ # has_one/has_many Associations
144
+ table.foreign_keys_to.sort_by{|fk| fk.describe}.map do |fk|
145
+ # Get the jump reference
146
+
147
+ if fk.from_columns.size > 1
148
+ raise "Can't emit Rails associations for multi-part foreign key with #{fk.references.inspect}. Did you mean to use --transform/surrogate"
149
+ end
150
+
151
+ association_type, association_name = *fk.rails_to_association
152
+
153
+ ref = fk.jump_reference
154
+ [
155
+ "\n \# #{fk.verbalised_path(true)}" +
156
+ "\n" +
157
+ %Q{ #{association_type} :#{association_name}} +
158
+ %Q{, :class_name => '#{fk.from.rails_class_name}'} +
159
+ %Q{, :foreign_key => :#{fk.from_columns[0].rails_name}} +
160
+ %Q{, :dependent => :destroy}
161
+ ] +
162
+ # If ref.from is a join table, we can emit a has_many :through for each other key
163
+ # REVISIT Could alternately do this for all belongs_to's in ref.from
164
+ if ref.from.identifier_columns.length > 1
165
+ ref.from.identifier_columns.map do |ic|
166
+ next nil if ic.references[0] == ref or # Skip the back-reference
167
+ ic.references[0].is_unary # or use rails_plural_name(ic.references[0].to_names) ?
168
+ # This far association name needs to be augmented for its role name
169
+ far_association_name = ic.references[0].to.rails_name
170
+ %Q{ has_many :#{far_association_name}, :through => :#{association_name}} # \# via #{ic.name}}
171
+ end
172
+ else
173
+ []
174
+ end
175
+ end.flatten.compact
176
+ end
177
+
178
+ def column_constraints table
179
+ return [] unless @validations
180
+ ccs =
181
+ table.columns.map do |column|
182
+ name = column.rails_name
183
+ column.is_mandatory &&
184
+ !column.is_auto_assigned && !column.is_auto_timestamp ? [
185
+ " validates :#{name}, :presence => true"
186
+ ] : []
187
+ end.flatten
188
+ ccs.unshift("") unless ccs.empty?
189
+ ccs
190
+ end
191
+
192
+ def model_body table
193
+ %Q{module #{table.rails_class_name}
194
+ extend ActiveSupport::Concern
195
+ included do} +
196
+ (table.identifier_columns.length == 1 ? %Q{
197
+ self.primary_key = '#{table.identifier_columns[0].rails_name}'
198
+ } : ''
199
+ ) +
200
+
201
+ (
202
+ to_associations(table) +
203
+ from_associations(table) +
204
+ column_constraints(table)
205
+ ) * "\n" +
206
+ %Q{
207
+ end
208
+ end
209
+ }
210
+ end
211
+
212
+ def generate_table table
213
+ old_out = @out
214
+ filename = table.rails_singular_name+'.rb'
215
+
216
+ return unless create_if_ok filename
217
+
218
+ puts "\n"
219
+ puts "module #{@concern}" if @concern
220
+ puts model_body(table).gsub(/^./, @concern ? ' \0' : '\0')
221
+ puts 'end' if @concern
222
+
223
+ true # We succeeded
224
+ ensure
225
+ @out = old_out
226
+ @individual_file.close if @individual_file
227
+ end
228
+
229
+ end
230
+ end
231
+ end
232
+
233
+ module RMap
234
+ class Column
235
+ def is_auto_timestamp
236
+ case name('_')
237
+ when /\A(created|updated)_(at|on)\Z/i
238
+ true
239
+ else
240
+ false
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
246
+
247
+ ActiveFacts::Registry.generator('rails/models', ActiveFacts::Generators::Rails::Models)