prailroady 1.5.3

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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.travis.yml +3 -0
  4. data/AUTHORS.rdoc +23 -0
  5. data/Gemfile +2 -0
  6. data/Gemfile.lock +33 -0
  7. data/Guardfile +49 -0
  8. data/LICENSE.rdoc +340 -0
  9. data/README.md +34 -0
  10. data/Rakefile +12 -0
  11. data/bin/prailroady +58 -0
  12. data/lib/prailroady.rb +5 -0
  13. data/lib/prailroady/aasm_diagram.rb +105 -0
  14. data/lib/prailroady/app_diagram.rb +103 -0
  15. data/lib/prailroady/controllers_diagram.rb +103 -0
  16. data/lib/prailroady/diagram_graph.rb +125 -0
  17. data/lib/prailroady/models_diagram.rb +357 -0
  18. data/lib/prailroady/options_struct.rb +204 -0
  19. data/lib/prailroady/railtie.rb +11 -0
  20. data/lib/prailroady/version.rb +3 -0
  21. data/prailroady.gemspec +25 -0
  22. data/tasks/prailroady.rake +139 -0
  23. data/test/file_fixture/app/controllers/application_controller.rb +2 -0
  24. data/test/file_fixture/app/controllers/dummy1_controller.rb +0 -0
  25. data/test/file_fixture/app/controllers/dummy2_controller.rb +0 -0
  26. data/test/file_fixture/app/controllers/sub-dir/sub_dummy_controller.rb +0 -0
  27. data/test/file_fixture/app/models/concerns/author_settings.rb +12 -0
  28. data/test/file_fixture/app/models/concerns/taggable.rb +0 -0
  29. data/test/file_fixture/app/models/dummy1.rb +0 -0
  30. data/test/file_fixture/app/models/dummy2.rb +0 -0
  31. data/test/file_fixture/app/models/sub-dir/sub_dummy.rb +0 -0
  32. data/test/file_fixture/lib/app/controllers/dummy/dummy_controller.rb +0 -0
  33. data/test/file_fixture/lib/app/models/dummy1.rb +0 -0
  34. data/test/lib/railroady/aasm_diagram_spec.rb +54 -0
  35. data/test/lib/railroady/app_diagram_spec.rb +14 -0
  36. data/test/lib/railroady/controllers_diagram_spec.rb +56 -0
  37. data/test/lib/railroady/core_ext_spec.rb +13 -0
  38. data/test/lib/railroady/diagram_graph_spec.rb +53 -0
  39. data/test/lib/railroady/models_diagram_spec.rb +162 -0
  40. data/test/spec_helper.rb +5 -0
  41. metadata +159 -0
@@ -0,0 +1,357 @@
1
+ # PrailRoady - RoR diagrams generator
2
+ # http://railroad.rubyforge.org
3
+ #
4
+ # Copyright 2007-2008 - Javier Smaldone (http://www.smaldone.com.ar)
5
+ # See COPYING for more details
6
+
7
+ require 'prailroady/app_diagram'
8
+
9
+ # PrailRoady models diagram
10
+ class ModelsDiagram < AppDiagram
11
+ def initialize(options = OptionsStruct.new)
12
+ super options
13
+ @graph.diagram_type = 'Models'
14
+ # Processed habtm associations
15
+ @habtm = []
16
+ end
17
+
18
+ # Process model files
19
+ def generate
20
+ STDERR.puts 'Generating models diagram' if @options.verbose
21
+ get_files.each do |f|
22
+ begin
23
+ process_class extract_class_name(f).constantize
24
+ rescue Exception
25
+ STDERR.puts "Warning: exception #{$ERROR_INFO} raised while trying to load model class #{f}"
26
+ end
27
+ end
28
+ end
29
+
30
+ def get_files(prefix = '')
31
+ files = !@options.specify.empty? ? Dir.glob(@options.specify) : Dir.glob(prefix + 'app/models/**/*.rb')
32
+ files += Dir.glob('vendor/plugins/**/app/models/*.rb') if @options.plugins_models
33
+ files -= Dir.glob(prefix + 'app/models/concerns/**/*.rb') unless @options.include_concerns
34
+ files += engine_files if @options.engine_models
35
+ files -= Dir.glob(@options.exclude)
36
+ files
37
+ end
38
+
39
+ def engine_files
40
+ engines.collect { |engine| Dir.glob("#{engine.root}/app/models/**/*.rb") }.flatten
41
+ end
42
+
43
+ def extract_class_name(filename)
44
+ filename_was, class_name = filename, nil
45
+
46
+ filename = "app/models/#{filename.split('app/models')[1]}"
47
+
48
+ while filename.split('/').length > 2
49
+ begin
50
+ class_name = filename.match(/.*\/models\/(.*).rb$/)[1].camelize
51
+ class_name.constantize
52
+
53
+ break
54
+ rescue Exception
55
+ class_name = nil
56
+ filename_end = filename.split('/')[2..-1]
57
+ filename_end.shift
58
+ filename = "#{filename.split('/')[0, 2].join('/')}/#{filename_end.join('/')}"
59
+ end
60
+ end
61
+
62
+ if class_name.nil?
63
+ filename_was.match(/.*\/models\/(.*).rb$/)[1].camelize
64
+ else
65
+ class_name
66
+ end
67
+ end
68
+
69
+ # Process a model class
70
+ def process_class(current_class)
71
+ STDERR.puts "Processing #{current_class}" if @options.verbose
72
+
73
+ generated =
74
+ if defined?(CouchRest::Model::Base) && current_class.new.is_a?(CouchRest::Model::Base)
75
+ process_couchrest_model(current_class)
76
+ elsif defined?(Mongoid::Document) && current_class.new.is_a?(Mongoid::Document)
77
+ process_mongoid_model(current_class)
78
+ elsif defined?(DataMapper::Resource) && current_class.new.is_a?(DataMapper::Resource)
79
+ process_datamapper_model(current_class)
80
+ elsif current_class.respond_to? 'reflect_on_all_associations'
81
+ process_active_record_model(current_class)
82
+ elsif @options.all && (current_class.is_a? Class)
83
+ process_basic_class(current_class)
84
+ elsif @options.modules && (current_class.is_a? Module)
85
+ process_basic_module(current_class)
86
+ end
87
+
88
+ if @options.inheritance && generated && include_inheritance?(current_class)
89
+ @graph.add_edge ['is-a', current_class.superclass.name, current_class.name]
90
+ end
91
+ end # process_class
92
+
93
+ def include_inheritance?(current_class)
94
+ STDERR.puts current_class.superclass if @options.verbose
95
+ (defined?(ActiveRecord::Base) ? current_class.superclass != ActiveRecord::Base : true) &&
96
+ (defined?(CouchRest::Model::Base) ? current_class.superclass != CouchRest::Model::Base : true) &&
97
+ (current_class.superclass != Object)
98
+ end
99
+
100
+ def process_basic_class(current_class)
101
+ node_type = @options.brief ? 'class-brief' : 'class'
102
+ @graph.add_node [node_type, current_class.name]
103
+ true
104
+ end
105
+
106
+ def process_basic_module(current_class)
107
+ @graph.add_node ['module', current_class.name]
108
+ false
109
+ end
110
+
111
+ def process_active_record_model(current_class)
112
+ node_attribs = []
113
+ if @options.brief || current_class.abstract_class?
114
+ node_type = 'model-brief'
115
+ else
116
+ node_type = 'model'
117
+
118
+ # Collect model's content columns
119
+ # content_columns = current_class.content_columns
120
+
121
+ if @options.hide_magic
122
+ # From patch #13351
123
+ # http://wiki.rubyonrails.org/rails/pages/MagicFieldNames
124
+ magic_fields = %w(created_at created_on updated_at updated_on lock_version type id position parent_id lft rgt quote template)
125
+ magic_fields << current_class.table_name + '_count' if current_class.respond_to? 'table_name'
126
+ content_columns = current_class.content_columns.select { |c| !magic_fields.include? c.name }
127
+ else
128
+ content_columns = current_class.columns
129
+ end
130
+
131
+ content_columns.each do |a|
132
+ content_column = a.name
133
+ content_column += ' :' + a.sql_type.to_s unless @options.hide_types
134
+ node_attribs << content_column
135
+ end
136
+ end
137
+ @graph.add_node [node_type, current_class.name, node_attribs]
138
+
139
+ # Process class associations
140
+ associations = current_class.reflect_on_all_associations
141
+ if @options.inheritance && ! @options.transitive
142
+ superclass_associations = current_class.superclass.reflect_on_all_associations
143
+
144
+ associations = associations.select { |a| !superclass_associations.include? a }
145
+ # This doesn't works!
146
+ # associations -= current_class.superclass.reflect_on_all_associations
147
+ end
148
+
149
+ associations.each do |a|
150
+ process_association current_class.name, a
151
+ end
152
+
153
+ true
154
+ end
155
+
156
+ def process_datamapper_model(current_class)
157
+ node_attribs = []
158
+ if @options.brief # || current_class.abstract_class?
159
+ node_type = 'model-brief'
160
+ else
161
+ node_type = 'model'
162
+
163
+ # Collect model's properties
164
+ props = current_class.properties.sort_by(&:name)
165
+
166
+ if @options.hide_magic
167
+ # From patch #13351
168
+ # http://wiki.rubyonrails.org/rails/pages/MagicFieldNames
169
+ magic_fields =
170
+ %w(created_at created_on updated_at updated_on lock_version _type _id position parent_id lft rgt quote template)
171
+ props = props.select { |c| !magic_fields.include?(c.name.to_s) }
172
+ end
173
+
174
+ props.each do |a|
175
+ prop = a.name.to_s
176
+ prop += ' :' + a.class.name.split('::').last unless @options.hide_types
177
+ node_attribs << prop
178
+ end
179
+ end
180
+ @graph.add_node [node_type, current_class.name, node_attribs]
181
+
182
+ # Process relationships
183
+ relationships = current_class.relationships
184
+
185
+ # TODO: Manage inheritance
186
+
187
+ relationships.each do |a|
188
+ process_datamapper_relationship current_class.name, a
189
+ end
190
+
191
+ true
192
+ end
193
+
194
+ def process_mongoid_model(current_class)
195
+ node_attribs = []
196
+
197
+ if @options.brief
198
+ node_type = 'model-brief'
199
+ else
200
+ node_type = 'model'
201
+
202
+ # Collect model's content columns
203
+ content_columns = current_class.fields.values.sort_by(&:name)
204
+
205
+ if @options.hide_magic
206
+ # From patch #13351
207
+ # http://wiki.rubyonrails.org/rails/pages/MagicFieldNames
208
+ magic_fields = %w(created_at created_on updated_at updated_on lock_version _type _id position parent_id lft rgt quote template)
209
+ content_columns = content_columns.select { |c| !magic_fields.include?(c.name) }
210
+ end
211
+
212
+ content_columns.each do |a|
213
+ content_column = a.name
214
+ content_column += " :#{a.type}" unless @options.hide_types
215
+ node_attribs << content_column
216
+ end
217
+ end
218
+
219
+ @graph.add_node [node_type, current_class.name, node_attribs]
220
+
221
+ # Process class associations
222
+ associations = current_class.relations.values
223
+
224
+ if @options.inheritance && !@options.transitive &&
225
+ current_class.superclass.respond_to?(:relations)
226
+ associations -= current_class.superclass.relations.values
227
+ end
228
+
229
+ associations.each do |a|
230
+ process_association current_class.name, a
231
+ end
232
+
233
+ true
234
+ end
235
+
236
+ ##
237
+ # Some very basic CouchRest::Model support
238
+ #
239
+ # Field types note: the field's type is rendered only if it's explicitly
240
+ # specified in a model.
241
+ #
242
+ def process_couchrest_model(current_class)
243
+ node_attribs = []
244
+
245
+ if @options.brief
246
+ node_type = 'model-brief'
247
+ else
248
+ node_type = 'model'
249
+
250
+ # Collect model's content columns
251
+ content_columns = current_class.properties
252
+
253
+ if @options.hide_magic
254
+ magic_fields = %w(created_at updated_at type _id _rev)
255
+ content_columns = content_columns.select { |c| !magic_fields.include?(c.name) }
256
+ end
257
+
258
+ content_columns.each do |a|
259
+ content_column = a.name
260
+ content_column += " :#{a.type}" unless @options.hide_types || a.type.nil?
261
+ node_attribs << content_column
262
+ end
263
+ end
264
+
265
+ @graph.add_node [node_type, current_class.name, node_attribs]
266
+
267
+ true
268
+ end
269
+
270
+ # Process a model association
271
+ def process_association(class_name, assoc)
272
+ STDERR.puts "- Processing model association #{assoc.name}" if @options.verbose
273
+
274
+ # Skip "belongs_to" associations
275
+ macro = assoc.macro.to_s
276
+ return if %w(belongs_to referenced_in).include?(macro) && !@options.show_belongs_to
277
+
278
+ # Skip "through" associations
279
+ through = assoc.options.include?(:through)
280
+ return if through && @options.hide_through
281
+
282
+ # TODO:
283
+ # FAIL: assoc.methods.include?(:class_name)
284
+ # FAIL: assoc.responds_to?(:class_name)
285
+ assoc_class_name = assoc.class_name rescue nil
286
+ assoc_class_name ||= assoc.name.to_s.underscore.singularize.camelize
287
+
288
+ # Only non standard association names needs a label
289
+
290
+ # from patch #12384
291
+ # if assoc.class_name == assoc.name.to_s.singularize.camelize
292
+ if assoc_class_name == assoc.name.to_s.singularize.camelize
293
+ assoc_name = ''
294
+ else
295
+ assoc_name = assoc.name.to_s
296
+ end
297
+
298
+ # Patch from "alpack" to support classes in a non-root module namespace. See: http://disq.us/yxl1v
299
+ if class_name.include?('::') && !assoc_class_name.include?('::')
300
+ assoc_class_name = class_name.split('::')[0..-2].push(assoc_class_name).join('::')
301
+ end
302
+ assoc_class_name.gsub!(/^::/, '')
303
+
304
+ if %w(has_one references_one embeds_one).include?(macro)
305
+ assoc_type = 'one-one'
306
+ elsif macro == 'has_many' && (!assoc.options[:through]) ||
307
+ %w(references_many embeds_many).include?(macro)
308
+ assoc_type = 'one-many'
309
+ elsif macro == 'belongs_to'
310
+ assoc_type = 'belongs-to'
311
+ else # habtm or has_many, :through
312
+ # Add FAKE associations too in order to understand mistakes
313
+ return if @habtm.include? [assoc_class_name, class_name, assoc_name]
314
+ assoc_type = 'many-many'
315
+ @habtm << [class_name, assoc_class_name, assoc_name]
316
+ end
317
+ # from patch #12384
318
+ # @graph.add_edge [assoc_type, class_name, assoc.class_name, assoc_name]
319
+ @graph.add_edge [assoc_type, class_name, assoc_class_name, assoc_name]
320
+ end # process_association
321
+
322
+ # Process a DataMapper relationship
323
+ def process_datamapper_relationship(class_name, relation)
324
+ STDERR.puts "- Processing DataMapper model relationship #{relation.name}" if @options.verbose
325
+
326
+ # Skip "belongs_to" relationships
327
+ dm_type = relation.class.to_s.split('::')[-2]
328
+ return if dm_type == 'ManyToOne' && !@options.show_belongs_to
329
+
330
+ dm_parent_model = relation.parent_model.to_s
331
+ dm_child_model = relation.child_model.to_s
332
+
333
+ assoc_class_name = ''
334
+ # Get the assoc_class_name
335
+ if dm_parent_model.eql?(class_name)
336
+ assoc_class_name = dm_child_model
337
+ else
338
+ assoc_class_name = dm_parent_model
339
+ end
340
+
341
+ # Only non standard association names needs a label
342
+ assoc_name = ''
343
+ unless relation.name.to_s.singularize.camelize.eql?(assoc_class_name.split('::').last)
344
+ assoc_name = relation.name.to_s
345
+ end
346
+
347
+ # TO BE IMPROVED
348
+ rel_type = 'many-many' # default value for ManyToOne and ManyToMany
349
+ if dm_type == 'OneToOne'
350
+ rel_type = 'one-one'
351
+ elsif dm_type == 'OneToMany'
352
+ rel_type = 'one-many'
353
+ end
354
+
355
+ @graph.add_edge [rel_type, class_name, assoc_class_name, assoc_name]
356
+ end
357
+ end # class ModelsDiagram
@@ -0,0 +1,204 @@
1
+ # PrailRoady - RoR diagrams generator
2
+ # http://railroad.rubyforge.org
3
+ #
4
+ # Copyright 2007-2008 - Javier Smaldone (http://www.smaldone.com.ar)
5
+ # See COPYING for more details
6
+
7
+ require 'ostruct'
8
+
9
+ # PrailRoady command line options parser
10
+ class OptionsStruct < OpenStruct
11
+ require 'optparse'
12
+
13
+ def initialize(args = {})
14
+ init_options = { all: false,
15
+ brief: false,
16
+ specify: [],
17
+ exclude: [],
18
+ inheritance: false,
19
+ join: false,
20
+ label: false,
21
+ modules: false,
22
+ all_columns: false,
23
+ hide_magic: false,
24
+ hide_types: false,
25
+ hide_public: false,
26
+ hide_protected: false,
27
+ hide_private: false,
28
+ plugins_models: false,
29
+ engine_models: false,
30
+ engine_controllers: false,
31
+ include_concerns: false,
32
+ root: '',
33
+ show_belongs_to: false,
34
+ hide_through: false,
35
+ transitive: false,
36
+ verbose: false,
37
+ alphabetize: false,
38
+ xmi: false,
39
+ command: '',
40
+ config_file: 'config/environment',
41
+ app_name: 'orailroady', app_human_name: 'PrailRoady', app_version: '', copyright: '' }
42
+ super(init_options.merge(args))
43
+ end # initialize
44
+
45
+ def parse(args)
46
+ @opt_parser = OptionParser.new do |opts|
47
+ opts.banner = "Usage: #{app_name} [options] command"
48
+ opts.separator ''
49
+ opts.separator 'Common options:'
50
+ opts.on('-b', '--brief', 'Generate compact diagram',
51
+ ' (no attributes nor methods)') do |b|
52
+ self.brief = b
53
+ end
54
+ opts.on('-s', '--specify file1[,fileN]', Array, 'Specify only given files') do |list|
55
+ self.specify = list
56
+ end
57
+ opts.on('-e', '--exclude file1[,fileN]', Array, 'Exclude given files') do |list|
58
+ self.exclude = list
59
+ end
60
+ opts.on('-i', '--inheritance', 'Include inheritance relations') do |i|
61
+ self.inheritance = i
62
+ end
63
+ opts.on('-l', '--label', 'Add a label with diagram information',
64
+ ' (type, date, migration, version)') do |l|
65
+ self.label = l
66
+ end
67
+ opts.on('-o', '--output FILE', 'Write diagram to file FILE') do |f|
68
+ self.output = f
69
+ end
70
+ opts.on('-r', '--root PATH', 'Set PATH as the application root') do |r|
71
+ self.root = r
72
+ end
73
+ opts.on('-v', '--verbose', 'Enable verbose output',
74
+ ' (produce messages to STDOUT)') do |v|
75
+ self.verbose = v
76
+ end
77
+ opts.on('-x', '--xmi', 'Produce XMI instead of DOT',
78
+ ' (for UML tools)') do |x|
79
+ self.xmi = x
80
+ end
81
+ opts.on('--alphabetize', 'Sort methods alphabetically') do |a|
82
+ self.alphabetize = a
83
+ end
84
+ opts.separator ''
85
+ opts.separator 'Models diagram options:'
86
+ opts.on('-a', '--all', 'Include all models',
87
+ ' (not only ActiveRecord::Base derived)') do |a|
88
+ self.all = a
89
+ end
90
+ opts.on('--show-belongs_to', 'Show belongs_to associations') do |s|
91
+ self.show_belongs_to = s
92
+ end
93
+ opts.on('--hide-through', 'Hide through associations') do |h|
94
+ self.hide_through = h
95
+ end
96
+ opts.on('--all-columns', 'Show all columns (not just content columns)') do |h|
97
+ self.all_columns = h
98
+ end
99
+ opts.on('--hide-magic', 'Hide magic field names') do |h|
100
+ self.hide_magic = h
101
+ end
102
+ opts.on('--hide-types', 'Hide attributes type') do |h|
103
+ self.hide_types = h
104
+ end
105
+ opts.on('-j', '--join', 'Concentrate edges') do |j|
106
+ self.join = j
107
+ end
108
+ opts.on('-m', '--modules', 'Include modules') do |m|
109
+ self.modules = m
110
+ end
111
+ opts.on('-p', '--plugins-models', 'Include plugins models') do |p|
112
+ self.plugins_models = p
113
+ end
114
+ opts.on('-z', '--engine-models', 'Include engine models') do |em|
115
+ self.engine_models = em
116
+ end
117
+ opts.on('--include-concerns', 'Include models in concerns subdirectory') do |c|
118
+ self.include_concerns = c
119
+ end
120
+ opts.on('-t', '--transitive', 'Include transitive associations',
121
+ '(through inheritance)') do |t|
122
+ self.transitive = t
123
+ end
124
+ opts.separator ''
125
+ opts.separator 'Controllers diagram options:'
126
+ opts.on('--hide-public', 'Hide public methods') do |h|
127
+ self.hide_public = h
128
+ end
129
+ opts.on('--hide-protected', 'Hide protected methods') do |h|
130
+ self.hide_protected = h
131
+ end
132
+ opts.on('--hide-private', 'Hide private methods') do |h|
133
+ self.hide_private = h
134
+ end
135
+ opts.on('--engine-controllers', 'Include engine controllers') do |ec|
136
+ self.engine_controllers = ec
137
+ end
138
+ opts.separator ''
139
+ opts.separator 'Other options:'
140
+ opts.on('-h', '--help', 'Show this message') do
141
+ STDOUT.print "#{opts}\n"
142
+ exit
143
+ end
144
+ opts.on('--version', 'Show version and copyright') do
145
+ STDOUT.print "#{app_human_name} version #{app_version}\n\n" \
146
+ "#{copyright}\nThis is free software; see the source " \
147
+ "for copying conditions.\n\n"
148
+ exit
149
+ end
150
+ opts.separator ''
151
+ opts.on('-c', '--config FILE', 'File to load environment (defaults to config/environment)') do |c|
152
+ self.config_file = c if c && c != ''
153
+ end
154
+ opts.separator 'Commands (you must supply one of these):'
155
+ opts.on('-M', '--models', 'Generate models diagram') do |_c|
156
+ if command != ''
157
+ STDERR.print "Error: Can only generate one diagram type\n\n"
158
+ exit 1
159
+ else
160
+ self.command = 'models'
161
+ end
162
+ end
163
+ opts.on('-C', '--controllers', 'Generate controllers diagram') do |_c|
164
+ if command != ''
165
+ STDERR.print "Error: Can only generate one diagram type\n\n"
166
+ exit 1
167
+ else
168
+ self.command = 'controllers'
169
+ end
170
+ end
171
+ # From Ana Nelson's patch
172
+ opts.on('-A', '--aasm', "Generate \"acts as state machine\" diagram") do |_c|
173
+ if command == 'controllers'
174
+ STDERR.print "Error: Can only generate one diagram type\n\n"
175
+ exit 1
176
+ else
177
+ self.command = 'aasm'
178
+ end
179
+ end
180
+ opts.separator ''
181
+ opts.separator 'For bug reporting and additional information, please see:'
182
+ opts.separator 'http://railroad.rubyforge.org/'
183
+ end # do
184
+
185
+ begin
186
+ @opt_parser.parse!(args)
187
+ rescue OptionParser::AmbiguousOption
188
+ option_error 'Ambiguous option'
189
+ rescue OptionParser::InvalidOption
190
+ option_error 'Invalid option'
191
+ rescue OptionParser::InvalidArgument
192
+ option_error 'Invalid argument'
193
+ rescue OptionParser::MissingArgument
194
+ option_error 'Missing argument'
195
+ end
196
+ end # parse
197
+
198
+ private
199
+
200
+ def option_error(msg)
201
+ STDERR.print "Error: #{msg}\n\n #{@opt_parser}\n"
202
+ exit 1
203
+ end
204
+ end # class OptionsStruct