muding 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/CHANGELOG +11 -0
  2. data/MANIFEST +77 -0
  3. data/MIT-LICENSE +22 -0
  4. data/README +21 -0
  5. data/bin/console +4 -0
  6. data/bin/destroy +4 -0
  7. data/bin/generate +4 -0
  8. data/bin/muding +16 -0
  9. data/bin/server +5 -0
  10. data/configs/boot.rb +4 -0
  11. data/configs/databases/mysql.yml +47 -0
  12. data/configs/databases/oracle.yml +30 -0
  13. data/configs/databases/postgresql.yml +44 -0
  14. data/configs/databases/sqlite2.yml +16 -0
  15. data/configs/databases/sqlite3.yml +16 -0
  16. data/doc/README_FOR_MUD +3 -0
  17. data/fresh_rakefile +4 -0
  18. data/helpers/mud.rb +8 -0
  19. data/helpers/mud_helper.rb +4 -0
  20. data/helpers/test_helper.rb +29 -0
  21. data/lib/acts/container.rb +70 -0
  22. data/lib/acts/expireable.rb +82 -0
  23. data/lib/commands/destroy.rb +7 -0
  24. data/lib/commands/generate.rb +7 -0
  25. data/lib/commands/server.rb +45 -0
  26. data/lib/commands/update.rb +5 -0
  27. data/lib/controller.rb +132 -0
  28. data/lib/handle.rb +42 -0
  29. data/lib/model.rb +11 -0
  30. data/lib/muding.rb +46 -0
  31. data/lib/muding_generator.rb +22 -0
  32. data/lib/muding_generator/base.rb +162 -0
  33. data/lib/muding_generator/commands.rb +519 -0
  34. data/lib/muding_generator/generators/applications/app/USAGE +14 -0
  35. data/lib/muding_generator/generators/applications/app/app_generator.rb +132 -0
  36. data/lib/muding_generator/generators/components/controller/USAGE +30 -0
  37. data/lib/muding_generator/generators/components/controller/controller_generator.rb +38 -0
  38. data/lib/muding_generator/generators/components/controller/templates/controller.rb +7 -0
  39. data/lib/muding_generator/generators/components/controller/templates/functional_test.rb +18 -0
  40. data/lib/muding_generator/generators/components/controller/templates/helper.rb +2 -0
  41. data/lib/muding_generator/generators/components/controller/templates/view.rhtml +2 -0
  42. data/lib/muding_generator/generators/components/migration/USAGE +14 -0
  43. data/lib/muding_generator/generators/components/migration/migration_generator.rb +7 -0
  44. data/lib/muding_generator/generators/components/migration/templates/migration.rb +7 -0
  45. data/lib/muding_generator/generators/components/model/USAGE +19 -0
  46. data/lib/muding_generator/generators/components/model/model_generator.rb +34 -0
  47. data/lib/muding_generator/generators/components/model/templates/fixtures.yml +5 -0
  48. data/lib/muding_generator/generators/components/model/templates/migration.rb +11 -0
  49. data/lib/muding_generator/generators/components/model/templates/model.rb +2 -0
  50. data/lib/muding_generator/generators/components/model/templates/unit_test.rb +10 -0
  51. data/lib/muding_generator/lookup.rb +210 -0
  52. data/lib/muding_generator/manifest.rb +53 -0
  53. data/lib/muding_generator/options.rb +140 -0
  54. data/lib/muding_generator/scripts.rb +83 -0
  55. data/lib/muding_generator/scripts/destroy.rb +7 -0
  56. data/lib/muding_generator/scripts/generate.rb +7 -0
  57. data/lib/muding_generator/scripts/update.rb +12 -0
  58. data/lib/muding_generator/simple_logger.rb +46 -0
  59. data/lib/muding_generator/spec.rb +44 -0
  60. data/lib/ruby_version_check.rb +12 -0
  61. data/lib/tasks/migrate.rake +33 -0
  62. metadata +149 -0
@@ -0,0 +1,519 @@
1
+ require 'delegate'
2
+ require 'optparse'
3
+ require 'fileutils'
4
+ require 'erb'
5
+
6
+ module Muding
7
+ module Generator
8
+ module Commands
9
+ # Here's a convenient way to get a handle on generator commands.
10
+ # Command.instance('destroy', my_generator) instantiates a Destroy
11
+ # delegate of my_generator ready to do your dirty work.
12
+ def self.instance(command, generator)
13
+ const_get(command.to_s.camelize).new(generator)
14
+ end
15
+
16
+ # Even more convenient access to commands. Include Commands in
17
+ # the generator Base class to get a nice #command instance method
18
+ # which returns a delegate for the requested command.
19
+ def self.append_features(base)
20
+ base.send(:define_method, :command) do |command|
21
+ Commands.instance(command, self)
22
+ end
23
+ end
24
+
25
+
26
+ # Generator commands delegate Rails::Generator::Base and implement
27
+ # a standard set of actions. Their behavior is defined by the way
28
+ # they respond to these actions: Create brings life; Destroy brings
29
+ # death; List passively observes.
30
+ #
31
+ # Commands are invoked by replaying (or rewinding) the generator's
32
+ # manifest of actions. See Rails::Generator::Manifest and
33
+ # Rails::Generator::Base#manifest method that generator subclasses
34
+ # are required to override.
35
+ #
36
+ # Commands allows generators to "plug in" invocation behavior, which
37
+ # corresponds to the GoF Strategy pattern.
38
+ class Base < DelegateClass(Muding::Generator::Base)
39
+ # Replay action manifest. RewindBase subclass rewinds manifest.
40
+ def invoke!
41
+ manifest.replay(self)
42
+ end
43
+
44
+ def dependency(generator_name, args, runtime_options = {})
45
+ logger.dependency(generator_name) do
46
+ self.class.new(instance(generator_name, args, full_options(runtime_options))).invoke!
47
+ end
48
+ end
49
+
50
+ # Does nothing for all commands except Create.
51
+ def class_collisions(*class_names)
52
+ end
53
+
54
+ # Does nothing for all commands except Create.
55
+ def readme(*args)
56
+ end
57
+
58
+ protected
59
+ def migration_directory(relative_path)
60
+ directory(@migration_directory = relative_path)
61
+ end
62
+
63
+ def existing_migrations(file_name)
64
+ Dir.glob("#{@migration_directory}/[0-9]*_*.rb").grep(/[0-9]+_#{file_name}.rb$/)
65
+ end
66
+
67
+ def migration_exists?(file_name)
68
+ not existing_migrations(file_name).empty?
69
+ end
70
+
71
+ def current_migration_number
72
+ Dir.glob("#{@migration_directory}/[0-9]*.rb").inject(0) do |max, file_path|
73
+ n = File.basename(file_path).split('_', 2).first.to_i
74
+ if n > max then n else max end
75
+ end
76
+ end
77
+
78
+ def next_migration_number
79
+ current_migration_number + 1
80
+ end
81
+
82
+ def next_migration_string(padding = 3)
83
+ "%.#{padding}d" % next_migration_number
84
+ end
85
+
86
+ private
87
+ # Ask the user interactively whether to force collision.
88
+ def force_file_collision?(destination)
89
+ $stdout.print "overwrite #{destination}? [Ynaq] "
90
+ case $stdin.gets
91
+ when /a/i
92
+ $stdout.puts "forcing #{spec.name}"
93
+ options[:collision] = :force
94
+ when /q/i
95
+ $stdout.puts "aborting #{spec.name}"
96
+ raise SystemExit
97
+ when /n/i then :skip
98
+ else :force
99
+ end
100
+ rescue
101
+ retry
102
+ end
103
+
104
+ def render_template_part(template_options)
105
+ # Getting Sandbox to evaluate part template in it
106
+ part_binding = template_options[:sandbox].call.sandbox_binding
107
+ part_rel_path = template_options[:insert]
108
+ part_path = source_path(part_rel_path)
109
+
110
+ # Render inner template within Sandbox binding
111
+ rendered_part = ERB.new(File.readlines(part_path).join, nil, '-').result(part_binding)
112
+ begin_mark = template_part_mark(template_options[:begin_mark], template_options[:mark_id])
113
+ end_mark = template_part_mark(template_options[:end_mark], template_options[:mark_id])
114
+ begin_mark + rendered_part + end_mark
115
+ end
116
+
117
+ def template_part_mark(name, id)
118
+ "<!--[#{name}:#{id}]-->\n"
119
+ end
120
+ end
121
+
122
+ # Base class for commands which handle generator actions in reverse, such as Destroy.
123
+ class RewindBase < Base
124
+ # Rewind action manifest.
125
+ def invoke!
126
+ manifest.rewind(self)
127
+ end
128
+ end
129
+
130
+
131
+ # Create is the premier generator command. It copies files, creates
132
+ # directories, renders templates, and more.
133
+ class Create < Base
134
+
135
+ # Check whether the given class names are already taken by
136
+ # Ruby or Rails. In the future, expand to check other namespaces
137
+ # such as the rest of the user's app.
138
+ def class_collisions(*class_names)
139
+ class_names.flatten.each do |class_name|
140
+ # Convert to string to allow symbol arguments.
141
+ class_name = class_name.to_s
142
+
143
+ # Skip empty strings.
144
+ next if class_name.strip.empty?
145
+
146
+ # Split the class from its module nesting.
147
+ nesting = class_name.split('::')
148
+ name = nesting.pop
149
+
150
+ # Extract the last Module in the nesting.
151
+ last = nesting.inject(Object) { |last, nest|
152
+ break unless last.const_defined?(nest)
153
+ last.const_get(nest)
154
+ }
155
+
156
+ # If the last Module exists, check whether the given
157
+ # class exists and raise a collision if so.
158
+ if last and last.const_defined?(name.camelize)
159
+ raise_class_collision(class_name)
160
+ end
161
+ end
162
+ end
163
+
164
+ # Copy a file from source to destination with collision checking.
165
+ #
166
+ # The file_options hash accepts :chmod and :shebang and :collision options.
167
+ # :chmod sets the permissions of the destination file:
168
+ # file 'config/empty.log', 'log/test.log', :chmod => 0664
169
+ # :shebang sets the #!/usr/bin/ruby line for scripts
170
+ # file 'bin/generate.rb', 'script/generate', :chmod => 0755, :shebang => '/usr/bin/env ruby'
171
+ # :collision sets the collision option only for the destination file:
172
+ # file 'settings/server.yml', 'config/server.yml', :collision => :skip
173
+ #
174
+ # Collisions are handled by checking whether the destination file
175
+ # exists and either skipping the file, forcing overwrite, or asking
176
+ # the user what to do.
177
+ def file(relative_source, relative_destination, file_options = {}, &block)
178
+ # Determine full paths for source and destination files.
179
+ source = source_path(relative_source)
180
+ destination = destination_path(relative_destination)
181
+ destination_exists = File.exists?(destination)
182
+
183
+ # If source and destination are identical then we're done.
184
+ if destination_exists and identical?(source, destination, &block)
185
+ return logger.identical(relative_destination)
186
+ end
187
+
188
+ # Check for and resolve file collisions.
189
+ if destination_exists
190
+
191
+ # Make a choice whether to overwrite the file. :force and
192
+ # :skip already have their mind made up, but give :ask a shot.
193
+ choice = case (file_options[:collision] || options[:collision]).to_sym #|| :ask
194
+ when :ask then force_file_collision?(relative_destination)
195
+ when :force then :force
196
+ when :skip then :skip
197
+ else raise "Invalid collision option: #{options[:collision].inspect}"
198
+ end
199
+
200
+ # Take action based on our choice. Bail out if we chose to
201
+ # skip the file; otherwise, log our transgression and continue.
202
+ case choice
203
+ when :force then logger.force(relative_destination)
204
+ when :skip then return(logger.skip(relative_destination))
205
+ else raise "Invalid collision choice: #{choice}.inspect"
206
+ end
207
+
208
+ # File doesn't exist so log its unbesmirched creation.
209
+ else
210
+ logger.create relative_destination
211
+ end
212
+
213
+ # If we're pretending, back off now.
214
+ return if options[:pretend]
215
+
216
+ # Write destination file with optional shebang. Yield for content
217
+ # if block given so templaters may render the source file. If a
218
+ # shebang is requested, replace the existing shebang or insert a
219
+ # new one.
220
+ File.open(destination, 'wb') do |df|
221
+ File.open(source, 'rb') do |sf|
222
+ if block_given?
223
+ df.write(yield(sf))
224
+ else
225
+ if file_options[:shebang]
226
+ df.puts("#!#{file_options[:shebang]}")
227
+ if line = sf.gets
228
+ df.puts(line) if line !~ /^#!/
229
+ end
230
+ end
231
+ df.write(sf.read)
232
+ end
233
+ end
234
+ end
235
+
236
+ # Optionally change permissions.
237
+ if file_options[:chmod]
238
+ FileUtils.chmod(file_options[:chmod], destination)
239
+ end
240
+
241
+ # Optionally add file to subversion
242
+ system("svn add #{destination}") if options[:svn]
243
+ end
244
+
245
+ # Checks if the source and the destination file are identical. If
246
+ # passed a block then the source file is a template that needs to first
247
+ # be evaluated before being compared to the destination.
248
+ def identical?(source, destination, &block)
249
+ return false if File.directory? destination
250
+ source = block_given? ? File.open(source) {|sf| yield(sf)} : IO.read(source)
251
+ destination = IO.read(destination)
252
+ source == destination
253
+ end
254
+
255
+ # Generate a file for a Rails application using an ERuby template.
256
+ # Looks up and evalutes a template by name and writes the result.
257
+ #
258
+ # The ERB template uses explicit trim mode to best control the
259
+ # proliferation of whitespace in generated code. <%- trims leading
260
+ # whitespace; -%> trims trailing whitespace including one newline.
261
+ #
262
+ # A hash of template options may be passed as the last argument.
263
+ # The options accepted by the file are accepted as well as :assigns,
264
+ # a hash of variable bindings. Example:
265
+ # template 'foo', 'bar', :assigns => { :action => 'view' }
266
+ #
267
+ # Template is implemented in terms of file. It calls file with a
268
+ # block which takes a file handle and returns its rendered contents.
269
+ def template(relative_source, relative_destination, template_options = {})
270
+ file(relative_source, relative_destination, template_options) do |file|
271
+ # Evaluate any assignments in a temporary, throwaway binding.
272
+ vars = template_options[:assigns] || {}
273
+ b = binding
274
+ vars.each { |k,v| eval "#{k} = vars[:#{k}] || vars['#{k}']", b }
275
+
276
+ # Render the source file with the temporary binding.
277
+ ERB.new(file.read, nil, '-').result(b)
278
+ end
279
+ end
280
+
281
+ def complex_template(relative_source, relative_destination, template_options = {})
282
+ options = template_options.dup
283
+ options[:assigns] ||= {}
284
+ options[:assigns]['template_for_inclusion'] = render_template_part(template_options)
285
+ template(relative_source, relative_destination, options)
286
+ end
287
+
288
+ # Create a directory including any missing parent directories.
289
+ # Always directories which exist.
290
+ def directory(relative_path)
291
+ path = destination_path(relative_path)
292
+ if File.exists?(path)
293
+ logger.exists relative_path
294
+ else
295
+ logger.create relative_path
296
+ FileUtils.mkdir_p(path) unless options[:pretend]
297
+
298
+ # Optionally add file to subversion
299
+ system("svn add #{path}") if options[:svn]
300
+ end
301
+ end
302
+
303
+ # Display a README.
304
+ def readme(*relative_sources)
305
+ relative_sources.flatten.each do |relative_source|
306
+ logger.readme relative_source
307
+ puts File.read(source_path(relative_source)) unless options[:pretend]
308
+ end
309
+ end
310
+
311
+ # When creating a migration, it knows to find the first available file in db/migrate and use the migration.rb template.
312
+ def migration_template(relative_source, relative_destination, template_options = {})
313
+ migration_directory relative_destination
314
+ migration_file_name = template_options[:migration_file_name] || file_name
315
+ raise "Another migration is already named #{migration_file_name}: #{existing_migrations(migration_file_name).first}" if migration_exists?(migration_file_name)
316
+ template(relative_source, "#{relative_destination}/#{next_migration_string}_#{migration_file_name}.rb", template_options)
317
+ end
318
+
319
+ private
320
+ # Raise a usage error with an informative WordNet suggestion.
321
+ # Thanks to Florian Gross (flgr).
322
+ def raise_class_collision(class_name)
323
+ message = <<end_message
324
+ The name '#{class_name}' is reserved by Ruby on Rails.
325
+ Please choose an alternative and run this generator again.
326
+ end_message
327
+ if suggest = find_synonyms(class_name)
328
+ message << "\n Suggestions: \n\n"
329
+ message << suggest.join("\n")
330
+ end
331
+ raise UsageError, message
332
+ end
333
+
334
+ SYNONYM_LOOKUP_URI = "http://wordnet.princeton.edu/cgi-bin/webwn2.0?stage=2&word=%s&posnumber=1&searchtypenumber=2&senses=&showglosses=1"
335
+
336
+ # Look up synonyms on WordNet. Thanks to Florian Gross (flgr).
337
+ def find_synonyms(word)
338
+ require 'open-uri'
339
+ require 'timeout'
340
+ timeout(5) do
341
+ open(SYNONYM_LOOKUP_URI % word) do |stream|
342
+ data = stream.read.gsub("&nbsp;", " ").gsub("<BR>", "")
343
+ data.scan(/^Sense \d+\n.+?\n\n/m)
344
+ end
345
+ end
346
+ rescue Exception
347
+ return nil
348
+ end
349
+ end
350
+
351
+
352
+ # Undo the actions performed by a generator. Rewind the action
353
+ # manifest and attempt to completely erase the results of each action.
354
+ class Destroy < RewindBase
355
+ # Remove a file if it exists and is a file.
356
+ def file(relative_source, relative_destination, file_options = {})
357
+ destination = destination_path(relative_destination)
358
+ if File.exists?(destination)
359
+ logger.rm relative_destination
360
+ unless options[:pretend]
361
+ if options[:svn]
362
+ # If the file has been marked to be added
363
+ # but has not yet been checked in, revert and delete
364
+ if options[:svn][relative_destination]
365
+ system("svn revert #{destination}")
366
+ FileUtils.rm(destination)
367
+ else
368
+ # If the directory is not in the status list, it
369
+ # has no modifications so we can simply remove it
370
+ system("svn rm #{destination}")
371
+ end
372
+ else
373
+ FileUtils.rm(destination)
374
+ end
375
+ end
376
+ else
377
+ logger.missing relative_destination
378
+ return
379
+ end
380
+ end
381
+
382
+ # Templates are deleted just like files and the actions take the
383
+ # same parameters, so simply alias the file method.
384
+ alias_method :template, :file
385
+
386
+ # Remove each directory in the given path from right to left.
387
+ # Remove each subdirectory if it exists and is a directory.
388
+ def directory(relative_path)
389
+ parts = relative_path.split('/')
390
+ until parts.empty?
391
+ partial = File.join(parts)
392
+ path = destination_path(partial)
393
+ if File.exists?(path)
394
+ if Dir[File.join(path, '*')].empty?
395
+ logger.rmdir partial
396
+ unless options[:pretend]
397
+ if options[:svn]
398
+ # If the directory has been marked to be added
399
+ # but has not yet been checked in, revert and delete
400
+ if options[:svn][relative_path]
401
+ system("svn revert #{path}")
402
+ FileUtils.rmdir(path)
403
+ else
404
+ # If the directory is not in the status list, it
405
+ # has no modifications so we can simply remove it
406
+ system("svn rm #{path}")
407
+ end
408
+ else
409
+ FileUtils.rmdir(path)
410
+ end
411
+ end
412
+ else
413
+ logger.notempty partial
414
+ end
415
+ else
416
+ logger.missing partial
417
+ end
418
+ parts.pop
419
+ end
420
+ end
421
+
422
+ def complex_template(*args)
423
+ # nothing should be done here
424
+ end
425
+
426
+ # When deleting a migration, it knows to delete every file named "[0-9]*_#{file_name}".
427
+ def migration_template(relative_source, relative_destination, template_options = {})
428
+ migration_directory relative_destination
429
+
430
+ migration_file_name = template_options[:migration_file_name] || file_name
431
+ unless migration_exists?(migration_file_name)
432
+ puts "There is no migration named #{migration_file_name}"
433
+ return
434
+ end
435
+
436
+
437
+ existing_migrations(migration_file_name).each do |file_path|
438
+ file(relative_source, file_path, template_options)
439
+ end
440
+ end
441
+ end
442
+
443
+
444
+ # List a generator's action manifest.
445
+ class List < Base
446
+ def dependency(generator_name, args, options = {})
447
+ logger.dependency "#{generator_name}(#{args.join(', ')}, #{options.inspect})"
448
+ end
449
+
450
+ def class_collisions(*class_names)
451
+ logger.class_collisions class_names.join(', ')
452
+ end
453
+
454
+ def file(relative_source, relative_destination, options = {})
455
+ logger.file relative_destination
456
+ end
457
+
458
+ def template(relative_source, relative_destination, options = {})
459
+ logger.template relative_destination
460
+ end
461
+
462
+ def complex_template(relative_source, relative_destination, options = {})
463
+ logger.template "#{options[:insert]} inside #{relative_destination}"
464
+ end
465
+
466
+ def directory(relative_path)
467
+ logger.directory "#{destination_path(relative_path)}/"
468
+ end
469
+
470
+ def readme(*args)
471
+ logger.readme args.join(', ')
472
+ end
473
+
474
+ def migration_template(relative_source, relative_destination, options = {})
475
+ migration_directory relative_destination
476
+ logger.migration_template file_name
477
+ end
478
+ end
479
+
480
+ # Update generator's action manifest.
481
+ class Update < Create
482
+ def file(relative_source, relative_destination, options = {})
483
+ # logger.file relative_destination
484
+ end
485
+
486
+ def template(relative_source, relative_destination, options = {})
487
+ # logger.template relative_destination
488
+ end
489
+
490
+ def complex_template(relative_source, relative_destination, template_options = {})
491
+
492
+ begin
493
+ dest_file = destination_path(relative_destination)
494
+ source_to_update = File.readlines(dest_file).join
495
+ rescue Errno::ENOENT
496
+ logger.missing relative_destination
497
+ return
498
+ end
499
+
500
+ logger.refreshing "#{template_options[:insert].gsub(/\.rhtml/,'')} inside #{relative_destination}"
501
+
502
+ begin_mark = Regexp.quote(template_part_mark(template_options[:begin_mark], template_options[:mark_id]))
503
+ end_mark = Regexp.quote(template_part_mark(template_options[:end_mark], template_options[:mark_id]))
504
+
505
+ # Refreshing inner part of the template with freshly rendered part.
506
+ rendered_part = render_template_part(template_options)
507
+ source_to_update.gsub!(/#{begin_mark}.*?#{end_mark}/m, rendered_part)
508
+
509
+ File.open(dest_file, 'w') { |file| file.write(source_to_update) }
510
+ end
511
+
512
+ def directory(relative_path)
513
+ # logger.directory "#{destination_path(relative_path)}/"
514
+ end
515
+ end
516
+
517
+ end
518
+ end
519
+ end