From ff2b6deceec7adb64ac13b6f33dbd0997795e7b8 Mon Sep 17 00:00:00 2001
From: zhandao <a@skipping.cat>
Date: Fri, 15 Mar 2024 15:45:36 +0800
Subject: [PATCH] Impl `style` option

---
 README.md                                     | 21 +++++---
 config/job_backend.yml                        |  5 ++
 config/styles/full/checkers.yml               |  0
 config/styles/full/code_snippets.yml          |  0
 config/styles/full/gems.yml                   |  0
 .../{job.yml => styles/full/job_backend.yml}  |  5 --
 config/styles/full/workflows.yml              |  0
 lib/nextgen.rb                                | 24 +++++++++
 lib/nextgen/cli.rb                            |  1 +
 lib/nextgen/commands/create.rb                | 51 ++++++-------------
 lib/nextgen/commands/helpers.rb               |  4 +-
 lib/nextgen/generators.rb                     | 12 ++---
 .../{job => job_backend}/sidekiq.rb           |  0
 .../{job => job_backend}/solid_queue.rb       |  0
 14 files changed, 66 insertions(+), 57 deletions(-)
 create mode 100644 config/job_backend.yml
 create mode 100644 config/styles/full/checkers.yml
 create mode 100644 config/styles/full/code_snippets.yml
 create mode 100644 config/styles/full/gems.yml
 rename config/{job.yml => styles/full/job_backend.yml} (70%)
 create mode 100644 config/styles/full/workflows.yml
 rename lib/nextgen/generators/{job => job_backend}/sidekiq.rb (100%)
 rename lib/nextgen/generators/{job => job_backend}/solid_queue.rb (100%)

diff --git a/README.md b/README.md
index fb53478..03343bf 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,13 @@ gem exec nextgen create myapp
 
 This will download the latest version of the `nextgen` gem and use it to create an app in the `myapp` directory. You'll be asked to configure the tech stack through several interactive prompts. If you have a `~/.railsrc` file, it will be ignored.
 
+Options:
+- `style`: control the **optional enhancements** you can choose in the generator.
+  - defaults to `default`, [enhancements list](config)
+  - presets:
+    - `full` (`--style=full`), [enhancements list](config/styles/full)
+  - your local configs: `--style=path/to/your/style_dir`
+
 > [!TIP]
 > If you get an "Unknown command exec" error, fix it by upgrading rubygems: `gem update --system`.
 
@@ -59,7 +66,7 @@ Check out the [examples directory](./examples) to see some Rails apps that were
 On top of that foundation, Nextgen offers dozens of useful enhancements to the vanilla Rails experience. You are free to pick and choose which (if any) of these to apply to your new project. Behind the scenes, **each enhancement is applied in a separate git commit,** so that you can later see what was applied and why, and revert the suggestions if necessary.
 
 > [!TIP]
-> For the full list of what Nextgen provides, check out [config/generators.yml](https://github.com/mattbrictson/nextgen/tree/main/config/generators.yml). The source code of each generator can be found in [lib/nextgen/generators](https://github.com/mattbrictson/nextgen/tree/main/lib/nextgen/generators).
+> For the full list of what Nextgen provides, check out [config/*.yml](https://github.com/mattbrictson/nextgen/tree/main/config). The source code of each generator can be found in [lib/nextgen/generators](https://github.com/mattbrictson/nextgen/tree/main/lib/nextgen/generators).
 
 Here are some highlights of what Nextgen brings to the table:
 
@@ -71,16 +78,14 @@ Nextgen can optionally set up a GitHub Actions CI workflow for your app that aut
 
 Prefer RSpec? Nextgen can set you up with RSpec, plus the gems and configuration you need for system specs (browser testing). Or stick with the Rails Minitest defaults. In either case, Nextgen will set up a good default Rake task and appropriate CI job.
 
-### Gems
-
-Nextgen can install and configure your choice of these recommended gems:
-
-#### Job Backends
+### Job Backends
 
 - [sidekiq](https://github.com/sidekiq/sidekiq)
-- [solid_queue](https://github.com/basecamp/solid_queue)
+- [solid_queue](https://github.com/basecamp/solid_queue) (`--style=full`)
+
+### Gems
 
-#### Other
+Nextgen can install and configure your choice of these recommended gems:
 
 - [annotate](https://github.com/ctran/annotate_models)
 - [brakeman](https://github.com/presidentbeef/brakeman)
diff --git a/config/job_backend.yml b/config/job_backend.yml
new file mode 100644
index 0000000..09ea387
--- /dev/null
+++ b/config/job_backend.yml
@@ -0,0 +1,5 @@
+
+sidekiq:
+  prompt: "Sidekiq (Redis-backed)"
+  description: "Install sidekiq gem to use in production"
+  requires: active_job
diff --git a/config/styles/full/checkers.yml b/config/styles/full/checkers.yml
new file mode 100644
index 0000000..e69de29
diff --git a/config/styles/full/code_snippets.yml b/config/styles/full/code_snippets.yml
new file mode 100644
index 0000000..e69de29
diff --git a/config/styles/full/gems.yml b/config/styles/full/gems.yml
new file mode 100644
index 0000000..e69de29
diff --git a/config/job.yml b/config/styles/full/job_backend.yml
similarity index 70%
rename from config/job.yml
rename to config/styles/full/job_backend.yml
index 71d2fe9..bfe741f 100644
--- a/config/job.yml
+++ b/config/styles/full/job_backend.yml
@@ -1,9 +1,4 @@
 
-sidekiq:
-  prompt: "Sidekiq (Redis-backed)"
-  description: "Install sidekiq gem to use in production"
-  requires: active_job
-
 solid_queue:
   prompt: "SolidQueue (Database-backed)"
   description: "Install solid_queue as ActiveJob's backend"
diff --git a/config/styles/full/workflows.yml b/config/styles/full/workflows.yml
new file mode 100644
index 0000000..e69de29
diff --git a/lib/nextgen.rb b/lib/nextgen.rb
index 7f8c2a2..79349e9 100644
--- a/lib/nextgen.rb
+++ b/lib/nextgen.rb
@@ -14,4 +14,28 @@ def self.generators_path(scope = "")
   def self.template_path
     Pathname.new(__dir__).join("../template")
   end
+
+  def self.config_path(style: nil)
+    if style
+      if style.match?("/")
+        Pathname.new(style)
+      else
+        Pathname.new(__dir__).join("../config/styles", style)
+      end
+    else
+      Pathname.new(__dir__).join("../config")
+    end
+  end
+
+  def self.config_for(scope:, style: nil)
+    base = YAML.load_file("#{Nextgen.config_path}/#{scope}.yml")
+    if style
+      base.merge!(YAML.load_file("#{Nextgen.config_path(style: style)}/#{scope}.yml") || {})
+    end
+    base
+  end
+
+  def self.scopes_for(style: nil)
+    Dir[Nextgen.config_path(style: style) + "*.yml"].map { _1.match(/([_a-z]*)\.yml/)[1] }
+  end
 end
diff --git a/lib/nextgen/cli.rb b/lib/nextgen/cli.rb
index 4ba5a94..a4d5197 100644
--- a/lib/nextgen/cli.rb
+++ b/lib/nextgen/cli.rb
@@ -6,6 +6,7 @@ class CLI < Thor
 
     map %w[-v --version] => "version"
 
+    option :style, type: :string, default: nil
     desc "create APP_PATH", "Generate a Rails app interactively in APP_PATH"
     def create(app_path)
       Commands::Create.run(app_path, options)
diff --git a/lib/nextgen/commands/create.rb b/lib/nextgen/commands/create.rb
index de7b086..9bb2b66 100644
--- a/lib/nextgen/commands/create.rb
+++ b/lib/nextgen/commands/create.rb
@@ -18,13 +18,14 @@ def self.run(app_path, options)
       new(app_path, options).run
     end
 
-    def initialize(app_path, _options)
+    def initialize(app_path, options)
       @app_path = File.expand_path(app_path)
       @app_name = File.basename(@app_path).gsub(/\W/, "_").squeeze("_").camelize
       @rails_opts = RailsOptions.new
+      @style = options[:style]
     end
 
-    def run # rubocop:disable Metrics/MethodLength Metrics/PerceivedComplexity
+    def run # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
       say_banner
       continue_if "Ready to start?"
 
@@ -39,12 +40,8 @@ def run # rubocop:disable Metrics/MethodLength Metrics/PerceivedComplexity
       ask_system_testing if rails_opts.frontend? && rails_opts.test_framework?
       say
 
-      if prompt.yes?("More detailed configuration? [ job, code snippets, gems ... ] ↵")
-        ask_job_backend if rails_opts.active_job?
-        ask_workflows
-        ask_checkers
-        ask_code_snippets
-        ask_optional_enhancements
+      if prompt.yes?("More enhancements? [ job, code snippets, gems ... ] ↵")
+        ask_styled_enhancements
       end
 
       say_summary
@@ -98,7 +95,7 @@ def ask_full_stack_or_api
         "API only" => true
       )
       rails_opts.api! if api
-      @generators = {basic: Generators.compatible_with(rails_opts: rails_opts, scope: "basic")}
+      @generators = {basic: Generators.compatible_with(rails_opts: rails_opts, style: nil, scope: "basic")}
     end
 
     def ask_frontend_management
@@ -189,33 +186,17 @@ def ask_system_testing
       rails_opts.skip_system_test! unless system_testing
     end
 
-    def ask_job_backend
-      generators[:job] = Generators.compatible_with(rails_opts: rails_opts, scope: "job").tap do |it|
-        it.ask_select("Which #{underline("job backend")} would you like to use?", prompt: prompt)
-      end
-    end
-
-    def ask_workflows
-      generators[:workflows] = Generators.compatible_with(rails_opts: rails_opts, scope: "workflows").tap do |it|
-        it.ask_select("Which #{underline("workflows")} would you like to add?", multi: true, prompt: prompt)
-      end
-    end
-
-    def ask_checkers
-      generators[:checkers] = Generators.compatible_with(rails_opts: rails_opts, scope: "checkers").tap do |it|
-        it.ask_select("Which #{underline("checkers")} would you like to add?", multi: true, prompt: prompt)
-      end
-    end
-
-    def ask_code_snippets
-      generators[:code_snippets] = Generators.compatible_with(rails_opts: rails_opts, scope: "code_snippets").tap do |it|
-        it.ask_select("Which #{underline("code snippets")} would you like to add?", multi: true, prompt: prompt)
-      end
-    end
+    def ask_styled_enhancements
+      say "  ↪ style: #{cyan(@style || "default")}"
+      Nextgen.scopes_for(style: @style).each do |scope|
+        gen = Generators.compatible_with(rails_opts: rails_opts, style: @style, scope: scope)
+        next if gen.empty? || scope == "basic"
 
-    def ask_optional_enhancements
-      generators[:gems] = Generators.compatible_with(rails_opts: rails_opts, scope: "gems").tap do |it|
-        it.ask_select("Which optional enhancements would you like to add?", multi: true, sort: true, prompt: prompt)
+        key_word = underline(scope.tr("_", " "))
+        multi = scope == scope.pluralize
+        sort = gen.optional.size > 10
+        gen.ask_select("Which #{key_word} would you like to add?", prompt: prompt, multi: multi, sort: sort)
+        generators[scope.to_sym] = gen
       end
     end
   end
diff --git a/lib/nextgen/commands/helpers.rb b/lib/nextgen/commands/helpers.rb
index 4005e27..fcf9566 100644
--- a/lib/nextgen/commands/helpers.rb
+++ b/lib/nextgen/commands/helpers.rb
@@ -110,9 +110,7 @@ def capture_version(command)
     end
 
     def activated_generators
-      activated = generators[:gems].all_active_names
-      activated.prepend(generators[:job].all_active_names.first) unless generators[:job].nil?
-
+      activated = generators.values.flat_map(&:all_active_names)
       activated.any? ? activated.sort_by(&:downcase) : ["<None>"]
     end
 
diff --git a/lib/nextgen/generators.rb b/lib/nextgen/generators.rb
index 6342167..0105871 100644
--- a/lib/nextgen/generators.rb
+++ b/lib/nextgen/generators.rb
@@ -2,10 +2,9 @@
 
 module Nextgen
   class Generators
-    def self.compatible_with(rails_opts:, scope:)
-      yaml_path = File.expand_path("../../config/#{scope}.yml", __dir__)
+    def self.compatible_with(rails_opts:, style:, scope:)
       new(scope, api: rails_opts.api?).tap do |generators|
-        YAML.load_file(yaml_path).each do |name, options|
+        Nextgen.config_for(style: style, scope: scope).each do |name, options|
           options ||= {}
           requirements = Array(options["requires"])
           next unless requirements.all? { |req| rails_opts.public_send(:"#{req}?") }
@@ -18,7 +17,6 @@ def self.compatible_with(rails_opts:, scope:)
             questions: options["questions"]
           )
         end
-
         generators.deactivate_node unless rails_opts.requires_node?
       end
     end
@@ -29,9 +27,11 @@ def initialize(scope, **vars)
       @scope = scope
     end
 
+    def empty? = @generators.empty?
+
     def ask_select(question, multi: false, sort: false, prompt: TTY::Prompt.new)
-      opt = sort ? optional.sort_by { |label, _| label.downcase }.to_h : optional
-      args = [question, opt, {cycle: true, filter: true}]
+      opts = sort ? optional.sort_by { |label, _| label.downcase }.to_h : optional
+      args = [question, opts, {cycle: true, filter: true}]
       answers = multi ? prompt.multi_select(*args) : [prompt.select(*args)]
 
       answers.each do |answer|
diff --git a/lib/nextgen/generators/job/sidekiq.rb b/lib/nextgen/generators/job_backend/sidekiq.rb
similarity index 100%
rename from lib/nextgen/generators/job/sidekiq.rb
rename to lib/nextgen/generators/job_backend/sidekiq.rb
diff --git a/lib/nextgen/generators/job/solid_queue.rb b/lib/nextgen/generators/job_backend/solid_queue.rb
similarity index 100%
rename from lib/nextgen/generators/job/solid_queue.rb
rename to lib/nextgen/generators/job_backend/solid_queue.rb