padrino-gen 0.17.0 → 0.18.0

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.rdoc +5 -5
  3. data/lib/padrino-gen/generators/actions.rb +145 -14
  4. data/lib/padrino-gen/generators/app/app.rb.tt +4 -2
  5. data/lib/padrino-gen/generators/cli.rb +9 -3
  6. data/lib/padrino-gen/generators/component.rb +19 -15
  7. data/lib/padrino-gen/generators/components/actions.rb +4 -3
  8. data/lib/padrino-gen/generators/components/mocks/mocha.rb +15 -9
  9. data/lib/padrino-gen/generators/components/mocks/rr.rb +24 -6
  10. data/lib/padrino-gen/generators/components/orms/activerecord.rb +47 -40
  11. data/lib/padrino-gen/generators/components/orms/mongoid.rb +28 -2
  12. data/lib/padrino-gen/generators/components/orms/sequel.rb +79 -36
  13. data/lib/padrino-gen/generators/components/renderers/liquid.rb +1 -2
  14. data/lib/padrino-gen/generators/components/scripts/dojo.rb +1 -1
  15. data/lib/padrino-gen/generators/components/scripts/jquery.rb +1 -1
  16. data/lib/padrino-gen/generators/components/scripts/mootools.rb +1 -1
  17. data/lib/padrino-gen/generators/components/scripts/prototype.rb +1 -1
  18. data/lib/padrino-gen/generators/components/stylesheets/sass.rb +1 -1
  19. data/lib/padrino-gen/generators/components/stylesheets/scss.rb +1 -1
  20. data/lib/padrino-gen/generators/components/tests/bacon.rb +3 -1
  21. data/lib/padrino-gen/generators/components/tests/cucumber.rb +13 -8
  22. data/lib/padrino-gen/generators/components/tests/minitest.rb +1 -1
  23. data/lib/padrino-gen/generators/components/tests/rspec.rb +1 -1
  24. data/lib/padrino-gen/generators/components/tests/shoulda.rb +1 -1
  25. data/lib/padrino-gen/generators/components/tests/testunit.rb +1 -1
  26. data/lib/padrino-gen/generators/project/config/apps.rb.tt +4 -1
  27. data/lib/padrino-gen/generators/project.rb +19 -5
  28. data/lib/padrino-gen/generators/runner.rb +3 -1
  29. data/lib/padrino-gen/padrino-tasks/activerecord.rb +62 -18
  30. data/lib/padrino-gen/padrino-tasks/sequel.rb +37 -14
  31. data/lib/padrino-gen/padrino-tasks/sql-helpers.rb +172 -44
  32. data/test/test_activerecord_tasks.rb +172 -2
  33. data/test/test_app_generator.rb +15 -2
  34. data/test/test_cli.rb +57 -0
  35. data/test/test_component_generator.rb +307 -3
  36. data/test/test_controller_generator.rb +3 -2
  37. data/test/test_generator.rb +24 -0
  38. data/test/test_helper_generator.rb +3 -2
  39. data/test/test_mailer_generator.rb +4 -3
  40. data/test/test_migration_generator.rb +124 -4
  41. data/test/test_model_generator.rb +106 -64
  42. data/test/test_plugin_generator.rb +45 -7
  43. data/test/test_project_generator.rb +168 -71
  44. data/test/test_sequel_tasks.rb +156 -0
  45. data/test/test_sql_helpers.rb +139 -64
  46. data/test/test_sql_helpers_dump.rb +328 -0
  47. data/test/test_task_generator.rb +4 -3
  48. metadata +12 -37
  49. data/lib/padrino-gen/generators/components/orms/dynamoid.rb +0 -67
  50. data/lib/padrino-gen/generators/components/orms/minirecord.rb +0 -184
  51. data/lib/padrino-gen/generators/components/orms/ohm.rb +0 -67
  52. data/lib/padrino-gen/generators/components/stylesheets/less.rb +0 -25
  53. data/lib/padrino-gen/padrino-tasks/minirecord.rb +0 -19
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4f2cbd5e5e7dbfaa0374bb83fad39536738fa404938ba59aa1296c65797a20c
4
- data.tar.gz: b10595d8cc86921a9c6682cfa633ecacbf9df607925903270ee57c67ca4e2d4e
3
+ metadata.gz: '039917b81dc2bfbb846774da7452682d6f29af8bc1f4d76609b2ec10d8392912'
4
+ data.tar.gz: 451d8a83d39747bf6648d0b434f211a4571e5694f5cc70b45e9f9844ef7bfa7c
5
5
  SHA512:
6
- metadata.gz: 513c6bfd2e9d9b8dde54280fd54fe33fb9f675fb213d14d3a14367f93ceaaf3395d060be2f42655fedeee875b0e0de2721caadcbba0ffffa7ede97e2f7bf50a5
7
- data.tar.gz: ddef4dd75dbe398a17faec64813e8d5f798969770148d9a9595aa327748db9ab603b2a49633ee7d74040d5d96b86acaddc69630fd561b39a9a8ede6cf302643c
6
+ metadata.gz: c0769c35ccfe2cff70e99aa0c137b825e285e831649945d18e07e5ea72b44efcc1214f3fb3ebc63661d0b0c8371e84c78dcb5fff6b7472471c83176e3dd1b40e
7
+ data.tar.gz: 45b9f905e803023d7932ceb487ade75bbcd5df395754f820cc86626176de272bb422ef5696d3665027b80f7a8f1d34436f266b02c8bb27d9938fbbcce410baf1
data/README.rdoc CHANGED
@@ -24,13 +24,13 @@ The simplest possible command to generate a base application would be:
24
24
 
25
25
  $ padrino-gen project demo_project
26
26
 
27
- This would construct a Padrino application DemoApp (which extends from Padrino::Application)
27
+ This would construct a Padrino application DemoProject::App (which extends from Padrino::Application)
28
28
  inside the folder 'demo_project' at our current path. Inside the application there would be configuration and
29
29
  setup performed for the default components.
30
30
 
31
31
  You can define specific components to be used:
32
32
 
33
- $ padrino-gen project demo_project -t rspec -r haml -m rr -s jquery -d sequel
33
+ $ padrino-gen project demo_project -t rspec -e haml -m rr -s jquery -d sequel
34
34
 
35
35
  You can also instruct the generator to skip a certain component to avoid using one at all (or to use your own):
36
36
 
@@ -39,11 +39,11 @@ You can also instruct the generator to skip a certain component to avoid using o
39
39
  The available components and their default options are listed below:
40
40
 
41
41
  test:: none (default), bacon, shoulda, cucumber, rspec, minitest, testunit
42
- renderer:: none (default), slim, erb, erubis, liquid, haml
43
- stylesheet:: none (default), tailwind, less, sass, scss
42
+ renderer:: none (default), slim, erb, liquid, haml
43
+ stylesheet:: none (default), tailwind, sass, scss
44
44
  mock:: none (default), mocha, rr
45
45
  script:: none (default), jquery, prototype, mootools, dojo, vite
46
- orm:: none (default), mongoid, activerecord, minirecord, sequel, ohm, dynamoid
46
+ orm:: none (default), mongoid, activerecord, sequel
47
47
 
48
48
  In addition, you can generate projects based on existing templates:
49
49
 
@@ -10,6 +10,12 @@ module Padrino
10
10
  #
11
11
  DEFAULT_HELPER_NAME = 'Helper'.freeze
12
12
 
13
+ ##
14
+ # The SQL adapters for --adapter, and the ORM components that set up their database with one.
15
+ #
16
+ SQL_ADAPTERS = %w[mysql mysql2 mysql-gem postgres sqlite].freeze
17
+ SQL_ADAPTER_ORMS = %w[activerecord sequel].freeze
18
+
13
19
  ##
14
20
  # Common actions needed to support project and component generation.
15
21
  #
@@ -59,6 +65,16 @@ module Padrino
59
65
  def apply_component_for(choice, component)
60
66
  # I need to override Thor#apply because for unknown reason verbose: false break tasks.
61
67
  path = File.expand_path(__dir__ + "/components/#{component.to_s.pluralize}/#{choice}.rb")
68
+ unless File.exist?(path)
69
+ # An existing project's .components can name a component that has since been removed.
70
+ available = Dir["#{__dir__}/components/#{component.to_s.pluralize}/*.rb"].map { |lib| File.basename(lib, '.rb') }.sort
71
+ if choice.to_s.empty?
72
+ say "The project's .components sets no #{component} component (available: #{available.join(', ')})."
73
+ else
74
+ say "The #{choice} #{component} component is no longer available in Padrino (available: #{available.join(', ')})."
75
+ end
76
+ exit 1
77
+ end
62
78
  say_status :apply, "#{component.to_s.pluralize}/#{choice}"
63
79
  shell.padding += 1
64
80
  instance_eval(File.read(path, encoding: 'UTF-8'))
@@ -157,10 +173,105 @@ module Padrino
157
173
  choice
158
174
  end
159
175
 
176
+ ##
177
+ # Prompts the user if necessary until options[:adapter] or the answer is a valid SQL adapter, and keeps it.
178
+ #
179
+ # @return [String] Name of the SQL adapter.
180
+ #
181
+ # @example
182
+ # resolve_valid_adapter # => 'sqlite'
183
+ #
184
+ def resolve_valid_adapter
185
+ @_adapter ||= begin
186
+ adapter = options[:adapter] || stored_adapter || 'sqlite'
187
+ until SQL_ADAPTERS.include?(adapter)
188
+ say("Option for --adapter '#{adapter}' is not available.", :red)
189
+ adapter = ask_valid_option(:adapter, SQL_ADAPTERS, 'Please enter a valid option for adapter:')
190
+ end
191
+ adapter
192
+ end
193
+ end
194
+
195
+ ##
196
+ # Returns whether `padrino g component` is applying a component the project already has,
197
+ # rather than adding it or switching to it. Files the project may have edited stay as they are then.
198
+ #
199
+ # @param [Symbol] component
200
+ # The type of component module.
201
+ #
202
+ # @return [Boolean] Whether the chosen component is the one the project has.
203
+ #
204
+ def reapplying_component?(component)
205
+ !@_stored_components.nil? && @_stored_components[component].to_s == @_components[component].to_s
206
+ end
207
+
208
+ ##
209
+ # Removes the lines the project's mock component added to the test helper, when `padrino g component`
210
+ # switches to another mock. Each mock file defines mock_setup_lines(test), the lines its setup_mock adds;
211
+ # the previous mock's file is evaluated on a separate object, so its methods don't replace the new mock's.
212
+ # Only lines that are still exactly as written (indentation aside) are removed; other edits stay.
213
+ #
214
+ # @param [String] test
215
+ # The test component the new mock is set up for.
216
+ #
217
+ def remove_previous_mock_setup(test)
218
+ previous = @_stored_components && @_stored_components[:mock].to_s
219
+ return if previous.nil? || previous.empty? || previous == 'none' || reapplying_component?(:mock)
220
+ source = File.expand_path("components/mocks/#{previous}.rb", __dir__)
221
+ return unless File.exist?(source)
222
+
223
+ probe = Object.new
224
+ probe.instance_eval(File.read(source, encoding: 'UTF-8'), source)
225
+ setup = probe.respond_to?(:mock_setup_lines) && probe.mock_setup_lines(test)
226
+ return unless setup && File.exist?(destination_root(setup[:path]))
227
+
228
+ content = File.binread(destination_root(setup[:path]))
229
+ # The mocks insert after the same line; if the project edited it, the new mock can't be added, so keep the old one.
230
+ return unless content.include?(setup[:after].b)
231
+
232
+ lines = setup[:text].each_line.map(&:strip).reject(&:empty?).uniq
233
+ pattern = /^[ \t]*(?:#{lines.map { |line| Regexp.escape(line) }.join('|')})[ \t]*(?:\n|\z)/
234
+ gsub_file(setup[:path], pattern, '') if content.match?(pattern)
235
+ end
236
+
237
+ ##
238
+ # Returns whether config/database.rb stays: the orm is the project's, and so is any --adapter given.
239
+ #
240
+ # @return [Boolean] Whether to keep an existing database config.
241
+ #
242
+ def keep_database_config?
243
+ # An --adapter corrected at the prompt counts as the corrected one, which resolve_valid_adapter keeps in @_adapter.
244
+ given = options[:adapter] && (@_adapter || options[:adapter])
245
+ reapplying_component?(:orm) && [nil, stored_adapter].include?(given)
246
+ end
247
+
248
+ ##
249
+ # Returns whether the SQL adapter of the project is unknown: it is applying the orm the project
250
+ # has, without --adapter, in a project that has a database config but no stored adapter
251
+ # (generated by an older version). The config stays and no driver is added then.
252
+ #
253
+ # @return [Boolean] Whether the adapter is unknown.
254
+ #
255
+ def adapter_unknown?
256
+ return @_adapter_unknown if defined?(@_adapter_unknown)
257
+
258
+ @_adapter_unknown = reapplying_component?(:orm) && options[:adapter].nil? && stored_adapter.nil? &&
259
+ File.exist?(destination_root('config/database.rb'))
260
+ end
261
+
262
+ ##
263
+ # Returns the SQL adapter stored for the project, if any. Projects generated by older versions have none.
264
+ #
265
+ # @return [String, nil] Name of the SQL adapter.
266
+ #
267
+ def stored_adapter
268
+ @_stored_components && @_stored_components[:adapter]
269
+ end
270
+
160
271
  ##
161
272
  # Asks once for one of the valid choices of an option; callers re-ask while the answer is invalid.
162
273
  # Thor's limited_to: would repeat the prompt forever at the end of input (no terminal, empty pipe),
163
- # so that case raises instead.
274
+ # so that case raises a Thor::Error instead: the generator's start prints its message and exits with status 1.
164
275
  #
165
276
  # @param [Symbol] option
166
277
  # The name of the option.
@@ -176,7 +287,7 @@ module Padrino
176
287
  #
177
288
  def ask_valid_option(option, choices, statement)
178
289
  answer = ask("#{statement} [#{choices.join(', ')}]")
179
- raise "No input left to choose a valid --#{option}; pass one of: #{choices.join(', ')}" if answer.nil?
290
+ raise Thor::Error, "No input left to choose a valid --#{option}; pass one of: #{choices.join(', ')}" if answer.nil?
180
291
 
181
292
  answer
182
293
  end
@@ -253,6 +364,10 @@ module Padrino
253
364
  def already_exists?(name, project_name = nil)
254
365
  project_name = project_name ? (Object.const_get(project_name) rescue nil) : nil
255
366
  Object.const_defined?(name) || project_name&.const_defined?(name)
367
+ rescue ::NameError => e
368
+ # Only Ruby's rejection of the name itself; a NameError from code that autoloading runs is a real error.
369
+ raise unless e.instance_of?(::NameError) && e.message.start_with?('wrong constant name')
370
+ raise Thor::Error, "#{name} is not a valid constant name"
256
371
  end
257
372
 
258
373
  ##
@@ -373,9 +488,18 @@ module Padrino
373
488
  # insert_into_gemfile(name, group: 'test', version: ">1.2.3")
374
489
  #
375
490
  def insert_into_gemfile(name, options = {})
376
- after_pattern = options[:group] ? "#{options[:group].to_s.capitalize} requirements\n" : "Component requirements\n"
377
- version = options.delete(:version)
378
- gem_options = options.map { |k, v| k.to_s == 'require' && [true, false].include?(v) ? "#{k}: #{v}" : "#{k}: '#{v}'" }.join(', ')
491
+ # Generated Gemfiles only have a "Test requirements" section; other groups go with the components.
492
+ group_pattern = "#{options[:group].to_s.capitalize} requirements\n" if options[:group]
493
+ gemfile = destination_root('Gemfile')
494
+ after_pattern = group_pattern if group_pattern && File.exist?(gemfile) && File.read(gemfile).include?(group_pattern)
495
+ after_pattern ||= "Component requirements\n"
496
+ version = options[:version]
497
+ gem_options = options.reject { |k, _| k == :version }.map do |k, v|
498
+ if v.is_a?(Array) then "#{k}: #{v.inspect}"
499
+ elsif k.to_s == 'require' && [true, false].include?(v) then "#{k}: #{v}"
500
+ else "#{k}: '#{v}'"
501
+ end
502
+ end.join(', ')
379
503
  write_option = gem_options.empty? ? '' : ", #{gem_options}"
380
504
  write_version = version ? ", '#{version}'" : ''
381
505
  include_text = "gem '#{name}'" << write_version << write_option << "\n"
@@ -418,16 +542,18 @@ module Padrino
418
542
  # Name of the initializer.
419
543
  # @param [String] data
420
544
  # Text to generate into the initializer file.
545
+ # @param [Boolean] skip
546
+ # Whether to keep an existing initializer file.
421
547
  #
422
548
  # @example
423
549
  # initializer(:test, "some stuff here")
424
550
  # #=> generates 'lib/test_init.rb'
425
551
  #
426
- def initializer(name, data = nil)
552
+ def initializer(name, data = nil, skip: false)
427
553
  @_init_name, @_init_data = name, data
428
554
  register = data ? " register #{name.to_s.underscore.camelize}Initializer\n" : " register #{name}\n"
429
555
  inject_into_file destination_root('/app/app.rb'), register, after: "Padrino::Application\n"
430
- template 'templates/initializer.rb.tt', destination_root("/config/initializers/#{name}.rb") if data
556
+ template 'templates/initializer.rb.tt', destination_root("/config/initializers/#{name}.rb"), skip: skip if data
431
557
  end
432
558
 
433
559
  ##
@@ -436,13 +562,15 @@ module Padrino
436
562
  # Name of the middleware.
437
563
  # @param [String] source
438
564
  # Text to generate into the middleware file.
565
+ # @param [Boolean] skip
566
+ # Whether to keep an existing middleware file.
439
567
  #
440
568
  # @example
441
569
  # middleware(:hello, "class Hello\nend")
442
570
  # #=> generates 'lib/hello_middleware.rb'
443
571
  #
444
- def middleware(name, source)
445
- create_file destination_root("lib/#{name}_middleware.rb"), source
572
+ def middleware(name, source, skip: false)
573
+ create_file destination_root("lib/#{name}_middleware.rb"), source, skip: skip
446
574
  insert_middleware name.to_s.underscore.camelize
447
575
  end
448
576
 
@@ -505,6 +633,8 @@ module Padrino
505
633
  saved = ENV.to_h
506
634
  if defined?(Bundler) && Bundler.respond_to?(:original_env)
507
635
  original = Bundler.original_env
636
+ # ceiling: a template's own changes to Bundler's keys (PATH, RUBYOPT, GEM_PATH...) made after Bundler loaded
637
+ # don't reach its steps; upgrade: re-apply the diff from a snapshot taken when the runner starts.
508
638
  BUNDLER_ENV_KEYS.each { |key| original.key?(key) ? ENV[key] = original[key] : ENV.delete(key) }
509
639
  ENV.keys.grep(/\ABUNDLER_ORIG_/).each { |key| ENV.delete(key) }
510
640
  end
@@ -565,7 +695,8 @@ module Padrino
565
695
  end
566
696
 
567
697
  ##
568
- # Ensures that project name is valid, else raise an NameError.
698
+ # Ensures that project name is valid, else raises a Thor::Error: the generator's start prints its
699
+ # message and exits with status 1.
569
700
  #
570
701
  # @param [String] name
571
702
  # Name of project.
@@ -578,9 +709,9 @@ module Padrino
578
709
  #
579
710
  def valid_constant?(name)
580
711
  if name =~ /^\d/
581
- raise ::NameError, "Constant name #{name} cannot start with numbers"
712
+ raise Thor::Error, "Constant name #{name} cannot start with numbers"
582
713
  elsif name =~ /^\W/
583
- raise ::NameError, "Constant name #{name} cannot start with non-word character"
714
+ raise Thor::Error, "Constant name #{name} cannot start with non-word character"
584
715
  end
585
716
  end
586
717
 
@@ -596,7 +727,7 @@ module Padrino
596
727
  #
597
728
  def validate_namespace(name)
598
729
  valid_constant? name
599
- name.match(/^[[:alnum:]_]+$/) || raise(::NameError, "Namespace '#{name}' must consist only of alphanumeric characters or '_'")
730
+ name.match(/^[[:alnum:]_]+$/) || raise(Thor::Error, "Namespace '#{name}' must consist only of alphanumeric characters or '_'")
600
731
  end
601
732
 
602
733
  ##
@@ -657,7 +788,7 @@ module Padrino
657
788
  [:stylesheet, 'stylesheet engine', { aliases: '-c', default: :none }]
658
789
  ].each do |name, caption, opts|
659
790
  opts[:default] = '' if options[:default] == false
660
- component_option name, caption, opts.merge(choices: Dir["#{__dir__}/components/#{name.to_s.pluralize}/*.rb"].map { |lib| File.basename(lib, '.rb').to_sym })
791
+ component_option name, caption, opts.merge(choices: Dir["#{__dir__}/components/#{name.to_s.pluralize}/*.rb"].map { |lib| File.basename(lib, '.rb') }.sort.map(&:to_sym))
661
792
  end
662
793
  end
663
794
 
@@ -4,8 +4,10 @@ module <%= @project_name %>
4
4
  register Padrino::Mailer
5
5
  register Padrino::Helpers
6
6
  <% end -%>
7
- set :sessions, same_site: :lax
8
- # set :sessions, same_site: :lax, secure: true # Send the session cookie over HTTPS only
7
+ # The session cookie is Secure (sent over HTTPS only) in production. Over plain HTTP
8
+ # the app then sends no session cookie, so logins and CSRF-protected forms fail. Behind
9
+ # a proxy that terminates TLS, have it send X-Forwarded-Proto: https, or set secure: false.
10
+ set :sessions, same_site: :lax, secure: Padrino.env == :production
9
11
 
10
12
  ##
11
13
  # Caching support.
@@ -19,10 +19,16 @@ module Padrino
19
19
  def load_boot
20
20
  saved_env = { 'BUNDLE_GEMFILE' => ENV['BUNDLE_GEMFILE'], 'PADRINO_LOG_LEVEL' => ENV['PADRINO_LOG_LEVEL'] }
21
21
  ENV['PADRINO_LOG_LEVEL'] ||= 'test'
22
- ENV['BUNDLE_GEMFILE'] = File.join(options[:root], 'Gemfile') if options[:root]
23
- boot = options[:root] ? File.join(options[:root], 'config/boot.rb') : 'config/boot.rb'
22
+ root = File.expand_path(options[:root] || '.')
23
+ ENV['BUNDLE_GEMFILE'] = File.join(root, 'Gemfile') if options[:root]
24
+ boot = File.join(root, 'config/boot.rb')
24
25
  if File.exist?(boot)
25
- require File.expand_path(boot)
26
+ # Boot from the project root, as `padrino start`, rake and the binstubs do, so that its relative paths
27
+ # (such as a sqlite database in config/database.rb) don't resolve against the caller's directory.
28
+ # ceiling: only the boot runs there; a path the project opens lazily after boot (an ActiveRecord connection
29
+ # to a relative path) still resolves against the caller's directory. Running the whole generator from the
30
+ # root would cover it.
31
+ Dir.chdir(root) { require boot }
26
32
  else
27
33
  require 'padrino-support'
28
34
  end
@@ -17,7 +17,7 @@ module Padrino
17
17
 
18
18
  class_option :app, desc: 'The application name', aliases: '-n', default: nil, type: :string
19
19
  class_option :root, desc: 'The root destination', aliases: '-r', default: '.', type: :string
20
- class_option :adapter, desc: 'SQL adapter for ORM (sqlite, mysql, mysql2, mysql-gem, postgres)', aliases: '-a', default: 'sqlite', type: :string
20
+ class_option :adapter, desc: 'SQL adapter for ORM (sqlite, mysql, mysql2, mysql-gem, postgres)', aliases: '-a', default: nil, type: :string
21
21
 
22
22
  defines_component_options default: false
23
23
 
@@ -28,6 +28,7 @@ module Padrino
28
28
  self.destination_root = options[:root]
29
29
  if in_app_root?
30
30
  @_components = options.class.new(options.slice(*self.class.component_types))
31
+ @_stored_components = retrieve_component_config(destination_root('.components'))
31
32
  @app_name = (options[:app] || 'App').gsub(/\W/, '_').camelize
32
33
  if @_components.values.delete_if(&:empty?).empty?
33
34
  self.class.start(['-h'])
@@ -41,31 +42,34 @@ module Padrino
41
42
  exit
42
43
  end
43
44
 
44
- missing_app = false
45
- self.class.component_types.each do |comp|
46
- next if @_components[comp].empty?
47
-
48
- choice = @_components[comp] = resolve_valid_choice(comp)
45
+ # Settle every choice, switch and check before applying any, so a failure leaves the project unchanged.
46
+ chosen = self.class.component_types.reject { |comp| @_components[comp].empty? }
47
+ chosen.each { |comp| @_components[comp] = resolve_valid_choice(comp) }
48
+ chosen.select! do |comp|
49
+ choice = @_components[comp]
49
50
  existing = fetch_component_choice(comp)
50
51
  ask = existing != 'none' && existing != choice
51
- next if ask && !yes?("Switch #{comp} to '#{choice}' from '#{existing}' ?[yes/no]:")
52
+ !ask || yes?("Switch #{comp} to '#{choice}' from '#{existing}' ?[yes/no]:")
53
+ end
52
54
 
53
- @project_name = fetch_component_choice(:namespace)
54
- if comp.to_s == 'test' && !already_exists?(@app_name, @project_name)
55
- say "#{@project_name}::#{@app_name} does not exist."
56
- say 'Please, change app name.'
57
- missing_app = true
58
- next
59
- end
55
+ @project_name = fetch_component_choice(:namespace)
56
+ if chosen.include?(:test) && !already_exists?(@app_name, @project_name)
57
+ say "#{@project_name}::#{@app_name} does not exist."
58
+ say 'Please, change app name.'
59
+ exit 1
60
+ end
61
+ resolve_valid_adapter if chosen.include?(:orm) && SQL_ADAPTER_ORMS.include?(@_components[:orm])
60
62
 
63
+ chosen.each do |comp|
64
+ choice = @_components[comp]
61
65
  execute_component_setup(comp, choice)
62
66
  store_component_choice(comp, choice)
67
+ store_component_choice(:adapter, resolve_valid_adapter) if comp.to_s == 'orm' && SQL_ADAPTER_ORMS.include?(choice) && !adapter_unknown?
63
68
  if comp.to_s == 'orm' && choice.to_s != 'none'
64
69
  inject_into_file destination_root('Rakefile'), "PadrinoTasks.use(:database)\n", before: 'PadrinoTasks.init'
65
70
  inject_into_file destination_root('Rakefile'), "PadrinoTasks.use(#{choice.to_sym.inspect})\n", before: 'PadrinoTasks.init'
66
71
  end
67
72
  end
68
- exit 1 if missing_app
69
73
  else
70
74
  exit_outside_app_root!
71
75
  end
@@ -28,7 +28,7 @@ module Padrino
28
28
  return if migration_exist?(filename)
29
29
  model_name = name.to_s.pluralize
30
30
  field_tuples = columns.map { |value| value.split(':') }
31
- field_tuples.map! { |field, kind, index| [field, kind =~ /datetime/i ? 'DateTime' : kind, index] }
31
+ field_tuples.map! { |field, kind, *modifiers| [field, kind =~ /datetime/i ? 'DateTime' : kind, *modifiers] }
32
32
  column_declarations = field_tuples.flat_map(&options[:column_format]).join("\n ")
33
33
  contents = options[:base].dup.gsub(/\s{4}!UP!\n/m, options[:up]).gsub(/!DOWN!\n/m, options[:down])
34
34
  contents = contents.gsub(/!NAME!/, model_name.underscore.camelize).gsub(/!TABLE!/, model_name.underscore)
@@ -70,7 +70,7 @@ module Padrino
70
70
  migration_scan = filename.underscore.camelize.scan(/(Add|Remove).*?(?:(?:To|From).+?)*(?:To|From)((?:To|From)?.*?)$/).flatten
71
71
  direction, table_name = migration_scan[0].downcase, migration_scan[1].downcase.pluralize if migration_scan.any?
72
72
  tuples = direction ? columns.map { |value| value.split(':') } : []
73
- tuples.map! { |field, kind, index| [field, kind =~ /datetime/i ? 'DateTime' : kind, index] }
73
+ tuples.map! { |field, kind, *modifiers| [field, kind =~ /datetime/i ? 'DateTime' : kind, *modifiers] }
74
74
  add_columns = tuples.flat_map(&options[:add]).join("\n ")
75
75
  remove_columns = tuples.flat_map(&options[:remove]).join("\n ")
76
76
  forward_text = change_format.gsub(/!TABLE!/, table_name).gsub(/!COLUMNS!/, add_columns) if tuples.any?
@@ -145,7 +145,8 @@ module Padrino
145
145
  #
146
146
  def insert_test_suite_setup(suite_text, options = {})
147
147
  options = { path: 'test/test_config.rb' }.update(options)
148
- create_file(options[:path], suite_text.gsub(/CLASS_NAME/, "#{@project_name}::#{@app_name}"))
148
+ # --app points the setup at another app, so that run rewrites it.
149
+ create_file(options[:path], suite_text.gsub(/CLASS_NAME/, "#{@project_name}::#{@app_name}"), skip: reapplying_component?(:test) && !self.options[:app])
149
150
  end
150
151
 
151
152
  ##
@@ -1,17 +1,23 @@
1
+ # The lines setup_mock adds for a test component: the helper file, the line they follow, and the lines.
2
+ # A switch to another mock removes them through remove_previous_mock_setup.
3
+ def mock_setup_lines(test)
4
+ {
5
+ 'rspec' => { path: 'spec/spec_helper.rb', after: "RSpec.configure do |conf|\n", text: " conf.mock_with :mocha\n" },
6
+ 'minitest' => { path: 'test/test_config.rb', after: "require_relative '../config/boot'\n", text: "require 'mocha/minitest'\n" },
7
+ 'testunit' => { path: 'test/test_config.rb', after: "require_relative '../config/boot'\n", text: "require 'mocha/test_unit'\n" },
8
+ 'shoulda' => { path: 'test/test_config.rb', after: "require \"test/unit\"\n", text: "require 'mocha/test_unit'\n" }
9
+ }[test.to_s]
10
+ end
11
+
1
12
  def setup_mock
2
13
  require_dependencies 'mocha', group: 'test', require: false
3
14
 
4
15
  test = options[:test].to_s
5
16
  test = fetch_component_choice(:test).to_s if test.empty?
6
- case test
7
- when 'rspec'
8
- inject_into_file 'spec/spec_helper.rb', " conf.mock_with :mocha\n", after: "RSpec.configure do |conf|\n"
9
- when 'minitest'
10
- inject_into_file 'test/test_config.rb', "require 'mocha/minitest'\n", after: "require_relative '../config/boot'\n"
11
- when 'testunit'
12
- inject_into_file 'test/test_config.rb', "require 'mocha/test_unit'\n", after: "require_relative '../config/boot'\n"
13
- when 'shoulda'
14
- inject_into_file 'test/test_config.rb', "require 'mocha/test_unit'\n", after: "require \"test/unit\"\n"
17
+ if (setup = mock_setup_lines(test))
18
+ inject_into_file setup[:path], setup[:text], after: setup[:after]
19
+ # After the insert, so a helper Thor fails to read keeps the previous mock.
20
+ remove_previous_mock_setup(test)
15
21
  else
16
22
  say "Mocha is in the Gemfile but not set up for the '#{test}' test component. To use it, require mocha/api " \
17
23
  'and call mocha_setup, mocha_verify and mocha_teardown around each test: https://github.com/freerange/mocha#readme', :yellow
@@ -1,11 +1,29 @@
1
+ # The lines setup_mock adds for a test component: the helper file, the line they follow, and the lines.
2
+ # A switch to another mock removes them through remove_previous_mock_setup.
3
+ def mock_setup_lines(test)
4
+ rspec = " conf.mock_with :rr\n" \
5
+ " # On ExampleGroup, not conf.include, so RSpec's own matchers (satisfy) stay ahead of rr's DSL.\n" \
6
+ " RSpec::Core::ExampleGroup.include RR::DSL\n"
7
+ {
8
+ 'rspec' => { path: 'spec/spec_helper.rb', after: "RSpec.configure do |conf|\n", text: rspec },
9
+ 'minitest' => { path: 'test/test_config.rb', after: "require_relative '../config/boot'\n", text: "require 'rr'\n" },
10
+ 'testunit' => { path: 'test/test_config.rb', after: "require_relative '../config/boot'\n", text: "require 'test/unit/rr'\n" },
11
+ 'shoulda' => { path: 'test/test_config.rb', after: "require \"test/unit\"\n", text: "require 'test/unit/rr'\n" }
12
+ }[test.to_s]
13
+ end
14
+
1
15
  def setup_mock
2
16
  require_dependencies 'rr', require: false, group: 'test'
3
- case options[:test].to_s
4
- when 'rspec'
5
- inject_into_file 'spec/spec_helper.rb', "require 'rr'\n", after: "'../config/boot'\n"
6
- when 'minitest'
7
- insert_mocking_include 'RR::Adapters::MiniTest', path: 'test/test_config.rb'
17
+
18
+ test = options[:test].to_s
19
+ test = fetch_component_choice(:test).to_s if test.empty?
20
+ require_dependencies 'test-unit-rr', require: false, group: 'test' if %w[testunit shoulda].include?(test)
21
+ if (setup = mock_setup_lines(test))
22
+ inject_into_file setup[:path], setup[:text], after: setup[:after]
23
+ # After the insert, so a helper Thor fails to read keeps the previous mock.
24
+ remove_previous_mock_setup(test)
8
25
  else
9
- insert_mocking_include 'RR::Adapters::TestUnit', path: 'test/test_config.rb'
26
+ say "RR is in the Gemfile but not set up for the '#{test}' test component. To use it, require 'rr', include RR::DSL " \
27
+ 'where your tests run, and call RR.reset before and RR.verify after each test: https://github.com/rr/rr#readme', :yellow
10
28
  end
11
29
  end
@@ -115,35 +115,27 @@ def setup_orm
115
115
  ar = AR.dup
116
116
  db = @project_name.underscore
117
117
 
118
- begin
119
- case adapter ||= options[:adapter]
120
- when 'mysql-gem'
121
- ar.sub!(/!DB_DEVELOPMENT!/, MYSQL.sub(/!DB_NAME!/, "'#{db}_development'"))
122
- ar.sub!(/!DB_PRODUCTION!/, MYSQL.sub(/!DB_NAME!/, "'#{db}_production'"))
123
- ar.sub!(/!DB_TEST!/, MYSQL.sub(/!DB_NAME!/, "'#{db}_test'"))
124
- require_dependencies 'mysql', version: '~> 2.8.1'
125
- when 'mysql', 'mysql2'
126
- ar.sub!(/!DB_DEVELOPMENT!/, MYSQL2.sub(/!DB_NAME!/, "'#{db}_development'"))
127
- ar.sub!(/!DB_PRODUCTION!/, MYSQL2.sub(/!DB_NAME!/, "'#{db}_production'"))
128
- ar.sub!(/!DB_TEST!/, MYSQL2.sub(/!DB_NAME!/, "'#{db}_test'"))
129
- require_dependencies 'mysql2'
130
- when 'postgres'
131
- ar.sub!(/!DB_DEVELOPMENT!/, POSTGRES.sub(/!DB_NAME!/, "'#{db}_development'"))
132
- ar.sub!(/!DB_PRODUCTION!/, POSTGRES.sub(/!DB_NAME!/, "'#{db}_production'"))
133
- ar.sub!(/!DB_TEST!/, POSTGRES.sub(/!DB_NAME!/, "'#{db}_test'"))
134
- require_dependencies 'pg'
135
- when 'sqlite'
136
- ar.sub!(/!DB_DEVELOPMENT!/, SQLITE.sub(/!DB_NAME!/, "Padrino.root('db', '#{db}_development.db')"))
137
- ar.sub!(/!DB_PRODUCTION!/, SQLITE.sub(/!DB_NAME!/, "Padrino.root('db', '#{db}_production.db')"))
138
- ar.sub!(/!DB_TEST!/, SQLITE.sub(/!DB_NAME!/, "Padrino.root('db', '#{db}_test.db')"))
139
- require_dependencies 'sqlite3'
140
- else
141
- say "Failed to generate `config/database.rb` for ORM adapter `#{adapter}`", :red
142
- raise ArgumentError
143
- end
144
- rescue ArgumentError
145
- adapter = ask_valid_option(:adapter, %w[mysql mysql2 mysql-gem postgres sqlite], 'Please, choose a proper adapter:')
146
- retry
118
+ case (resolve_valid_adapter unless adapter_unknown?)
119
+ when 'mysql-gem'
120
+ ar.sub!(/!DB_DEVELOPMENT!/, MYSQL.sub(/!DB_NAME!/, "'#{db}_development'"))
121
+ ar.sub!(/!DB_PRODUCTION!/, MYSQL.sub(/!DB_NAME!/, "'#{db}_production'"))
122
+ ar.sub!(/!DB_TEST!/, MYSQL.sub(/!DB_NAME!/, "'#{db}_test'"))
123
+ require_dependencies 'mysql', version: '~> 2.8.1'
124
+ when 'mysql', 'mysql2'
125
+ ar.sub!(/!DB_DEVELOPMENT!/, MYSQL2.sub(/!DB_NAME!/, "'#{db}_development'"))
126
+ ar.sub!(/!DB_PRODUCTION!/, MYSQL2.sub(/!DB_NAME!/, "'#{db}_production'"))
127
+ ar.sub!(/!DB_TEST!/, MYSQL2.sub(/!DB_NAME!/, "'#{db}_test'"))
128
+ require_dependencies 'mysql2'
129
+ when 'postgres'
130
+ ar.sub!(/!DB_DEVELOPMENT!/, POSTGRES.sub(/!DB_NAME!/, "'#{db}_development'"))
131
+ ar.sub!(/!DB_PRODUCTION!/, POSTGRES.sub(/!DB_NAME!/, "'#{db}_production'"))
132
+ ar.sub!(/!DB_TEST!/, POSTGRES.sub(/!DB_NAME!/, "'#{db}_test'"))
133
+ require_dependencies 'pg'
134
+ when 'sqlite'
135
+ ar.sub!(/!DB_DEVELOPMENT!/, SQLITE.sub(/!DB_NAME!/, "Padrino.root('db', '#{db}_development.db')"))
136
+ ar.sub!(/!DB_PRODUCTION!/, SQLITE.sub(/!DB_NAME!/, "Padrino.root('db', '#{db}_production.db')"))
137
+ ar.sub!(/!DB_TEST!/, SQLITE.sub(/!DB_NAME!/, "Padrino.root('db', '#{db}_test.db')"))
138
+ require_dependencies 'sqlite3'
147
139
  end
148
140
 
149
141
  require_dependencies 'activerecord', require: 'active_record', version: '>= 3.1'
@@ -156,8 +148,8 @@ def setup_orm
156
148
  FIBER
157
149
  end
158
150
 
159
- create_file('config/database.rb', ar)
160
- middleware :connection_pool_management, CONNECTION_POOL_MIDDLEWARE
151
+ create_file('config/database.rb', ar, skip: keep_database_config?)
152
+ middleware :connection_pool_management, CONNECTION_POOL_MIDDLEWARE, skip: reapplying_component?(:orm)
161
153
  end
162
154
 
163
155
  AR_MODEL = <<~MODEL unless defined?(AR_MODEL)
@@ -214,11 +206,24 @@ def ar_reference?(kind)
214
206
  %w[references belongs_to].include?(kind.underscore)
215
207
  end
216
208
 
217
- # A third field segment, as in user_id:integer:index or email:string:uniq, adds an index.
218
- def ar_add_column(field, kind, index)
209
+ # Segments after the kind are modifiers, in any order: index or uniq adds one index (uniq wins),
210
+ # as in user_id:integer:index or email:string:uniq. On a reference polymorphic adds a type column,
211
+ # as in commentable:references:polymorphic, and combines with an index: owner:references:polymorphic:uniq.
212
+ def ar_index_modifier(modifiers)
213
+ if modifiers.include?('uniq') then 'uniq'
214
+ elsif modifiers.include?('index') then 'index'
215
+ end
216
+ end
217
+
218
+ def ar_polymorphic_option(modifiers)
219
+ ', polymorphic: true' if modifiers.include?('polymorphic')
220
+ end
221
+
222
+ def ar_add_column(field, kind, modifiers)
223
+ index = ar_index_modifier(modifiers)
219
224
  if ar_reference?(kind)
220
225
  index_option = { 'index' => ', index: true', 'uniq' => ', index: { unique: true }' }[index]
221
- return "t.#{kind.underscore} :#{field}#{index_option}"
226
+ return "t.#{kind.underscore} :#{field}#{ar_polymorphic_option(modifiers)}#{index_option}"
222
227
  end
223
228
 
224
229
  columns = ["t.#{kind.underscore.gsub(/_/, '')} :#{field}"]
@@ -227,16 +232,18 @@ def ar_add_column(field, kind, index)
227
232
  columns
228
233
  end
229
234
 
230
- # Removing a column also removes its indexes.
231
- def ar_remove_column(field, kind)
232
- ar_reference?(kind) ? "t.remove_references :#{field}" : "t.remove :#{field}"
235
+ # Removing a column also removes its indexes. A polymorphic reference also drops its type column.
236
+ def ar_remove_column(field, kind, modifiers)
237
+ return "t.remove :#{field}" unless ar_reference?(kind)
238
+
239
+ "t.remove_references :#{field}#{ar_polymorphic_option(modifiers)}"
233
240
  end
234
241
 
235
242
  def create_model_migration(migration_name, name, columns)
236
243
  output_model_migration(
237
244
  migration_name, name, columns,
238
245
  base: AR_MIGRATION,
239
- column_format: proc { |field, kind, index| ar_add_column(field, kind, index) },
246
+ column_format: proc { |field, kind, *modifiers| ar_add_column(field, kind, modifiers) },
240
247
  up: AR_MODEL_UP_MG,
241
248
  down: AR_MODEL_DOWN_MG
242
249
  )
@@ -253,7 +260,7 @@ def create_migration_file(migration_name, name, columns)
253
260
  migration_name, name, columns,
254
261
  base: AR_MIGRATION,
255
262
  change_format: AR_CHANGE_MG,
256
- add: proc { |field, kind, index| ar_add_column(field, kind, index) },
257
- remove: proc { |field, kind| ar_remove_column(field, kind) }
263
+ add: proc { |field, kind, *modifiers| ar_add_column(field, kind, modifiers) },
264
+ remove: proc { |field, kind, *modifiers| ar_remove_column(field, kind, modifiers) }
258
265
  )
259
266
  end