activefacts-generators 1.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 (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)