gemsmith 15.5.0 → 16.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (129) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE.adoc +207 -155
  4. data/README.adoc +177 -189
  5. data/exe/gemsmith +6 -0
  6. data/gemsmith.gemspec +41 -0
  7. data/lib/gemsmith/builders/bundler.rb +34 -0
  8. data/lib/gemsmith/builders/cli.rb +86 -0
  9. data/lib/gemsmith/builders/git/commit.rb +46 -0
  10. data/lib/gemsmith/builders/git/ignore.rb +37 -0
  11. data/lib/gemsmith/builders/rspec/helper.rb +47 -0
  12. data/lib/gemsmith/builders/specification.rb +35 -0
  13. data/lib/gemsmith/cli/actions/build.rb +53 -0
  14. data/lib/gemsmith/cli/actions/config.rb +35 -0
  15. data/lib/gemsmith/cli/actions/edit.rb +38 -0
  16. data/lib/gemsmith/cli/actions/install.rb +36 -0
  17. data/lib/gemsmith/cli/actions/publish.rb +36 -0
  18. data/lib/gemsmith/cli/actions/view.rb +38 -0
  19. data/lib/gemsmith/cli/parser.rb +33 -0
  20. data/lib/gemsmith/cli/parsers/build.rb +54 -0
  21. data/lib/gemsmith/cli/parsers/core.rb +94 -0
  22. data/lib/gemsmith/cli/shell.rb +66 -0
  23. data/lib/gemsmith/configuration/enhancers/template_root.rb +20 -0
  24. data/lib/gemsmith/configuration/loader.rb +50 -0
  25. data/lib/gemsmith/container.rb +21 -0
  26. data/lib/gemsmith/gems/finder.rb +21 -0
  27. data/lib/gemsmith/gems/loader.rb +21 -0
  28. data/lib/gemsmith/gems/picker.rb +43 -0
  29. data/lib/gemsmith/gems/presenter.rb +50 -0
  30. data/lib/gemsmith/templates/%project_name%/%project_name%.gemspec.erb +49 -0
  31. data/lib/gemsmith/templates/%project_name%/exe/%project_name%.erb +5 -0
  32. data/lib/gemsmith/templates/%project_name%/lib/%project_path%/cli/actions/config.rb.erb +35 -0
  33. data/lib/gemsmith/templates/%project_name%/lib/%project_path%/cli/parser.rb.erb +31 -0
  34. data/lib/gemsmith/templates/%project_name%/lib/%project_path%/cli/parsers/core.rb.erb +54 -0
  35. data/lib/gemsmith/templates/%project_name%/lib/%project_path%/cli/shell.rb.erb +38 -0
  36. data/lib/gemsmith/templates/%project_name%/lib/%project_path%/configuration/content.rb.erb +18 -0
  37. data/lib/gemsmith/templates/%project_name%/lib/%project_path%/configuration/defaults.yml.erb +0 -0
  38. data/lib/gemsmith/templates/%project_name%/lib/%project_path%/configuration/loader.rb.erb +33 -0
  39. data/lib/gemsmith/templates/%project_name%/lib/%project_path%/container.rb.erb +35 -0
  40. data/lib/gemsmith/templates/{%gem_name%/lib/%gem_path%/identity.rb.tt → %project_name%/lib/%project_path%/identity.rb.erb} +1 -1
  41. data/lib/gemsmith/templates/%project_name%/spec/lib/%project_path%/cli/actions/config_spec.rb.erb +24 -0
  42. data/lib/gemsmith/templates/%project_name%/spec/lib/%project_path%/cli/parser_spec.rb.erb +25 -0
  43. data/lib/gemsmith/templates/%project_name%/spec/lib/%project_path%/cli/parsers/core_spec.rb.erb +53 -0
  44. data/lib/gemsmith/templates/%project_name%/spec/lib/%project_path%/cli/shell_spec.rb.erb +43 -0
  45. data/lib/gemsmith/templates/%project_name%/spec/lib/%project_path%/configuration/content_spec.rb.erb +15 -0
  46. data/lib/gemsmith/templates/%project_name%/spec/lib/%project_path%/configuration/loader_spec.rb.erb +29 -0
  47. data/lib/gemsmith/templates/%project_name%/spec/support/shared_contexts/application_container.rb.erb +22 -0
  48. data/lib/gemsmith/templates/%project_name%/spec/support/shared_examples/a_parser.rb.erb +7 -0
  49. data/lib/gemsmith/tools/cleaner.rb +29 -0
  50. data/lib/gemsmith/tools/editor.rb +30 -0
  51. data/lib/gemsmith/tools/installer.rb +39 -0
  52. data/lib/gemsmith/tools/packager.rb +33 -0
  53. data/lib/gemsmith/tools/publisher.rb +34 -0
  54. data/lib/gemsmith/tools/pusher.rb +36 -0
  55. data/lib/gemsmith/tools/validator.rb +28 -0
  56. data/lib/gemsmith/tools/versioner.rb +43 -0
  57. data/lib/gemsmith/tools/viewer.rb +28 -0
  58. data/lib/gemsmith.rb +5 -3
  59. data.tar.gz.sig +0 -0
  60. metadata +87 -115
  61. metadata.gz.sig +0 -0
  62. data/bin/gemsmith +0 -9
  63. data/lib/gemsmith/authenticators/basic.rb +0 -28
  64. data/lib/gemsmith/authenticators/ruby_gems.rb +0 -41
  65. data/lib/gemsmith/cli.rb +0 -259
  66. data/lib/gemsmith/credentials.rb +0 -82
  67. data/lib/gemsmith/errors/base.rb +0 -9
  68. data/lib/gemsmith/errors/requirement_conversion.rb +0 -9
  69. data/lib/gemsmith/errors/requirement_operator.rb +0 -9
  70. data/lib/gemsmith/errors/specification.rb +0 -9
  71. data/lib/gemsmith/gem/inspector.rb +0 -30
  72. data/lib/gemsmith/gem/module_formatter.rb +0 -50
  73. data/lib/gemsmith/gem/requirement.rb +0 -55
  74. data/lib/gemsmith/gem/specification.rb +0 -74
  75. data/lib/gemsmith/generators/base.rb +0 -46
  76. data/lib/gemsmith/generators/bundler.rb +0 -19
  77. data/lib/gemsmith/generators/bundler_audit.rb +0 -15
  78. data/lib/gemsmith/generators/circle_ci.rb +0 -14
  79. data/lib/gemsmith/generators/cli.rb +0 -17
  80. data/lib/gemsmith/generators/documentation.rb +0 -36
  81. data/lib/gemsmith/generators/engine.rb +0 -77
  82. data/lib/gemsmith/generators/gem.rb +0 -29
  83. data/lib/gemsmith/generators/git.rb +0 -45
  84. data/lib/gemsmith/generators/git_hub.rb +0 -15
  85. data/lib/gemsmith/generators/git_lint.rb +0 -14
  86. data/lib/gemsmith/generators/guard.rb +0 -14
  87. data/lib/gemsmith/generators/pragma.rb +0 -49
  88. data/lib/gemsmith/generators/rake.rb +0 -76
  89. data/lib/gemsmith/generators/reek.rb +0 -17
  90. data/lib/gemsmith/generators/rspec.rb +0 -39
  91. data/lib/gemsmith/generators/rubocop.rb +0 -18
  92. data/lib/gemsmith/generators/ruby.rb +0 -12
  93. data/lib/gemsmith/helpers/cli.rb +0 -59
  94. data/lib/gemsmith/helpers/template.rb +0 -30
  95. data/lib/gemsmith/identity.rb +0 -12
  96. data/lib/gemsmith/rake/builder.rb +0 -57
  97. data/lib/gemsmith/rake/publisher.rb +0 -100
  98. data/lib/gemsmith/rake/setup.rb +0 -4
  99. data/lib/gemsmith/rake/tasks.rb +0 -83
  100. data/lib/gemsmith/templates/%gem_name%/%gem_name%.gemspec.tt +0 -44
  101. data/lib/gemsmith/templates/%gem_name%/.circleci/config.yml.tt +0 -31
  102. data/lib/gemsmith/templates/%gem_name%/.github/ISSUE_TEMPLATE.md.tt +0 -14
  103. data/lib/gemsmith/templates/%gem_name%/.github/PULL_REQUEST_TEMPLATE.md.tt +0 -11
  104. data/lib/gemsmith/templates/%gem_name%/.gitignore.tt +0 -6
  105. data/lib/gemsmith/templates/%gem_name%/.reek.yml.tt +0 -6
  106. data/lib/gemsmith/templates/%gem_name%/.rubocop.yml.tt +0 -15
  107. data/lib/gemsmith/templates/%gem_name%/.ruby-version.tt +0 -1
  108. data/lib/gemsmith/templates/%gem_name%/CHANGES.md.tt +0 -3
  109. data/lib/gemsmith/templates/%gem_name%/CODE_OF_CONDUCT.md.tt +0 -66
  110. data/lib/gemsmith/templates/%gem_name%/CONTRIBUTING.md.tt +0 -22
  111. data/lib/gemsmith/templates/%gem_name%/Gemfile.tt +0 -47
  112. data/lib/gemsmith/templates/%gem_name%/Guardfile.tt +0 -21
  113. data/lib/gemsmith/templates/%gem_name%/LICENSE.md.tt +0 -20
  114. data/lib/gemsmith/templates/%gem_name%/README.md.tt +0 -93
  115. data/lib/gemsmith/templates/%gem_name%/Rakefile.tt +0 -12
  116. data/lib/gemsmith/templates/%gem_name%/bin/%gem_name%.tt +0 -8
  117. data/lib/gemsmith/templates/%gem_name%/bin/console.tt +0 -10
  118. data/lib/gemsmith/templates/%gem_name%/bin/setup.tt +0 -8
  119. data/lib/gemsmith/templates/%gem_name%/lib/%gem_path%/cli.rb.tt +0 -61
  120. data/lib/gemsmith/templates/%gem_name%/lib/%gem_path%/engine.rb.tt +0 -6
  121. data/lib/gemsmith/templates/%gem_name%/lib/%gem_path%.rb.tt +0 -7
  122. data/lib/gemsmith/templates/%gem_name%/lib/generators/%gem_path%/install/USAGE.tt +0 -8
  123. data/lib/gemsmith/templates/%gem_name%/lib/generators/%gem_path%/install/install_generator.rb.tt +0 -12
  124. data/lib/gemsmith/templates/%gem_name%/lib/generators/%gem_path%/upgrade/USAGE.tt +0 -8
  125. data/lib/gemsmith/templates/%gem_name%/lib/generators/%gem_path%/upgrade/upgrade_generator.rb.tt +0 -10
  126. data/lib/gemsmith/templates/%gem_name%/spec/lib/%gem_path%/cli_spec.rb.tt +0 -81
  127. data/lib/gemsmith/templates/%gem_name%/spec/rails_helper.rb.tt +0 -14
  128. data/lib/gemsmith/templates/%gem_name%/spec/spec_helper.rb.tt +0 -37
  129. data/lib/gemsmith/templates/%gem_name%/spec/support/shared_contexts/temp_dir.rb.tt +0 -9
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "refinements/arrays"
5
+ require "refinements/hashes"
6
+ require "refinements/structs"
7
+ require "runcom"
8
+ require "yaml"
9
+
10
+ module Gemsmith
11
+ module Configuration
12
+ # Represents the fully assembled Command Line Interface (CLI) configuration.
13
+ class Loader
14
+ using Refinements::Arrays
15
+ using Refinements::Hashes
16
+ using Refinements::Structs
17
+
18
+ DEFAULTS = Rubysmith::Configuration::Loader::DEFAULTS
19
+ CLIENT = Runcom::Config.new "gemsmith/configuration.yml", defaults: DEFAULTS
20
+
21
+ ENHANCERS = Rubysmith::Configuration::Loader::ENHANCERS.including(Enhancers::TemplateRoot.new)
22
+ .freeze
23
+
24
+ def self.call(...) = new(...).call
25
+
26
+ def self.with_defaults = new(client: DEFAULTS, enhancers: [])
27
+
28
+ def self.with_overrides = new(client: DEFAULTS, enhancers: [Enhancers::TemplateRoot.new])
29
+
30
+ def initialize content: Rubysmith::Configuration::Content.new,
31
+ client: CLIENT,
32
+ enhancers: ENHANCERS
33
+ @content = content
34
+ @client = client
35
+ @enhancers = enhancers
36
+ end
37
+
38
+ def call
39
+ enhancers.reduce(preload_content) { |preload, enhancer| enhancer.call preload }
40
+ .freeze
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :content, :client, :enhancers
46
+
47
+ def preload_content = content.merge(**client.to_h.flatten_keys)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-container"
4
+ require "open3"
5
+
6
+ module Gemsmith
7
+ # Provides a global gem container for injection into other objects.
8
+ module Container
9
+ extend Dry::Container::Mixin
10
+
11
+ config.registry = ->(container, key, value, _options) { container[key.to_s] = value }
12
+
13
+ merge Rubysmith::Container
14
+
15
+ register(:configuration) { Gemsmith::Configuration::Loader.call }
16
+ register(:specification) { Gemsmith::Gems::Loader.call "#{__dir__}/../../gemsmith.gemspec" }
17
+ register(:environment) { ENV }
18
+ register(:executor) { Open3 }
19
+ register(:kernel) { Kernel }
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gemsmith
4
+ module Gems
5
+ # Finds multiple versions of an installed gem (if any) and answers found specifications.
6
+ class Finder
7
+ def self.call(path, ...) = new(...).call path
8
+
9
+ def initialize client: ::Gem::Specification, presenter: Gems::Presenter
10
+ @client = client
11
+ @presenter = presenter
12
+ end
13
+
14
+ def call(name) = client.find_all_by_name(name).map { |record| presenter.new record }
15
+
16
+ private
17
+
18
+ attr_reader :client, :presenter
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gemsmith
4
+ module Gems
5
+ # Loads a gem's specification.
6
+ class Loader
7
+ def self.call(path, ...) = new(...).call path
8
+
9
+ def initialize client: ::Gem::Specification, presenter: Gems::Presenter
10
+ @client = client
11
+ @presenter = presenter
12
+ end
13
+
14
+ def call(path) = client.load(path.to_s).then { |record| presenter.new record }
15
+
16
+ private
17
+
18
+ attr_reader :client, :presenter
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads"
4
+
5
+ module Gemsmith
6
+ module Gems
7
+ # Picks a gem specification.
8
+ class Picker
9
+ include Dry::Monads[:result]
10
+
11
+ def self.call(name, ...) = new(...).call name
12
+
13
+ def initialize finder: Finder.new, kernel: Kernel
14
+ @finder = finder
15
+ @kernel = kernel
16
+ end
17
+
18
+ def call name
19
+ specifications = finder.call name
20
+
21
+ case specifications.size
22
+ when 1 then Success specifications.first
23
+ when 2.. then Success choose(specifications)
24
+ else Failure "Unknown gem or gem is not installed: #{name}."
25
+ end
26
+ end
27
+
28
+ def choose specifications
29
+ specifications.each.with_index 1 do |specification, index|
30
+ kernel.puts "#{index}. #{specification.named_version}"
31
+ end
32
+
33
+ kernel.puts "\nPlease enter gem selection:"
34
+ ARGV.clear
35
+ specifications[kernel.gets.chomp.to_i - 1]
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :finder, :kernel
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "pathname"
5
+ require "refinements/arrays"
6
+ require "versionaire"
7
+
8
+ module Gemsmith
9
+ module Gems
10
+ # Provides a gem specification that is more useful than what is provided Ruby Gems.
11
+ class Presenter
12
+ extend Forwardable
13
+
14
+ using Refinements::Arrays
15
+ using Versionaire::Cast
16
+
17
+ delegate %i[metadata name summary] => :record
18
+
19
+ def initialize record
20
+ @record = record
21
+ end
22
+
23
+ def allowed_push_key = metadata.fetch "allowed_push_key", "rubygems_api_key"
24
+
25
+ def allowed_push_host = metadata.fetch "allowed_push_host", ::Gem::DEFAULT_HOST
26
+
27
+ def homepage_url = String record.homepage
28
+
29
+ def label = metadata.fetch "label", "Undefined"
30
+
31
+ def labeled_summary(delimiter: " - ") = [label, summary].compress.join delimiter
32
+
33
+ def labeled_version = "#{label} #{version}"
34
+
35
+ def named_version = "#{name} #{version}"
36
+
37
+ def package_path = Pathname("tmp").join package_name
38
+
39
+ def package_name = "#{name}-#{version}.gem"
40
+
41
+ def source_path = Pathname record.full_gem_path
42
+
43
+ def version = Version record.version.to_s
44
+
45
+ private
46
+
47
+ attr_reader :record
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,49 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "<%= configuration.project_name %>"
3
+ spec.version = "<%= configuration.project_version %>"
4
+ spec.platform = Gem::Platform::RUBY
5
+ spec.authors = ["<%= configuration.author_name %>"]
6
+ spec.email = ["<%= configuration.author_email %>"]
7
+ spec.homepage = "<%= configuration.computed_project_url_home %>"
8
+ spec.summary = ""
9
+ spec.license = "<%= configuration.license_label_version %>"
10
+
11
+ spec.metadata = {
12
+ "bug_tracker_uri" => "<%= configuration.computed_project_url_issues %>",
13
+ "changelog_uri" => "<%= configuration.computed_project_url_versions %>",
14
+ "documentation_uri" => "<%= configuration.computed_project_url_home %>",
15
+ "label" => "<%= configuration.project_label %>",
16
+ "rubygems_mfa_required" => "true",
17
+ "source_code_uri" => "<%= configuration.computed_project_url_source %>"
18
+ }
19
+
20
+ <% if configuration.build_security %>
21
+ spec.signing_key = Gem.default_key_path
22
+ spec.cert_chain = [Gem.default_cert_path]
23
+ <% end %>
24
+
25
+ spec.required_ruby_version = "~> <%= RUBY_VERSION[/\d+\.\d+/] %>"
26
+ <% if configuration.build_cli %>
27
+ spec.add_dependency "dry-container", "~> 0.9"
28
+ <% end %>
29
+ <% if configuration.build_cli %>
30
+ spec.add_dependency "pastel", "~> 0.8"
31
+ <% end %>
32
+ <% if configuration.build_refinements %>
33
+ spec.add_dependency "refinements", "~> 9.1"
34
+ <% end %>
35
+ <% if configuration.build_cli %>
36
+ spec.add_dependency "runcom", "~> 8.0"
37
+ <% end %>
38
+ <% if configuration.build_zeitwerk %>
39
+ spec.add_dependency "zeitwerk", "~> 2.5"
40
+ <% end %>
41
+
42
+ <% if configuration.build_cli %>
43
+ spec.bindir = "exe"
44
+ spec.executables << "<%= configuration.project_name %>"
45
+ <% end %>
46
+ spec.extra_rdoc_files = Dir["README*", "LICENSE*"]
47
+ spec.files = Dir["*.gemspec", "lib/**/*"]
48
+ spec.require_paths = ["lib"]
49
+ end
@@ -0,0 +1,5 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require "<%= configuration.project_name %>"
4
+
5
+ <%= configuration.project_class %>::CLI::Shell.new.call ARGV
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% namespace do %>
4
+ module CLI
5
+ module Actions
6
+ # Handles the config action.
7
+ class Config
8
+ def initialize client: Configuration::Loader::CLIENT, container: Container
9
+ @client = client
10
+ @container = container
11
+ end
12
+
13
+ def call selection
14
+ case selection
15
+ when :edit then edit
16
+ when :view then view
17
+ else logger.error { "Invalid configuration selection: #{selection}." }
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :client, :container
24
+
25
+ def edit = kernel.system("$EDITOR #{client.current}")
26
+
27
+ def view = kernel.system("cat #{client.current}")
28
+
29
+ def kernel = container[__method__]
30
+
31
+ def logger = container[__method__]
32
+ end
33
+ end
34
+ end
35
+ <% end %>
@@ -0,0 +1,31 @@
1
+ require "optparse"
2
+
3
+ <% namespace do %>
4
+ module CLI
5
+ # Assembles and parses all Command Line Interface (CLI) options.
6
+ class Parser
7
+ CLIENT = OptionParser.new nil, 40, " "
8
+
9
+ # Order is important.
10
+ SECTIONS = [Parsers::Core].freeze
11
+
12
+ def initialize sections: SECTIONS, client: CLIENT, container: Container
13
+ @sections = sections
14
+ @client = client
15
+ @configuration = container[:configuration].dup
16
+ end
17
+
18
+ def call arguments = []
19
+ sections.each { |section| section.call configuration, client: }
20
+ client.parse arguments
21
+ configuration.freeze
22
+ end
23
+
24
+ def to_s = client.to_s
25
+
26
+ private
27
+
28
+ attr_reader :sections, :client, :configuration
29
+ end
30
+ end
31
+ <% end %>
@@ -0,0 +1,54 @@
1
+ require "refinements/structs"
2
+
3
+ <% namespace do %>
4
+ module CLI
5
+ module Parsers
6
+ # Handles parsing of Command Line Interface (CLI) core options.
7
+ class Core
8
+ using Refinements::Structs
9
+
10
+ def self.call(...) = new(...).call
11
+
12
+ def initialize configuration = Container[:configuration], client: Parser::CLIENT
13
+ @configuration = configuration
14
+ @client = client
15
+ end
16
+
17
+ def call arguments = []
18
+ client.banner = "<%= configuration.project_label %>"
19
+ client.separator "\nUSAGE:\n"
20
+ collate
21
+ client.parse arguments
22
+ configuration
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :configuration, :client
28
+
29
+ def collate = private_methods.sort.grep(/add_/).each { |method| __send__ method }
30
+
31
+ def add_config
32
+ client.on "-c",
33
+ "--config ACTION",
34
+ %i[edit view],
35
+ "Manage gem configuration: edit or view." do |action|
36
+ configuration.merge! action_config: action
37
+ end
38
+ end
39
+
40
+ def add_version
41
+ client.on "-v", "--version", "Show gem version." do
42
+ configuration.merge! action_version: true
43
+ end
44
+ end
45
+
46
+ def add_help
47
+ client.on "-h", "--help", "Show this message." do
48
+ configuration.merge! action_help: true
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ <% end %>
@@ -0,0 +1,38 @@
1
+ <% namespace do %>
2
+ module CLI
3
+ # The main Command Line Interface (CLI) object.
4
+ class Shell
5
+ ACTIONS = {config: Actions::Config.new}.freeze
6
+
7
+ def initialize parser: Parser.new, actions: ACTIONS, container: Container
8
+ @parser = parser
9
+ @actions = actions
10
+ @container = container
11
+ end
12
+
13
+ def call arguments = []
14
+ perform parser.call(arguments)
15
+ rescue OptionParser::ParseError => error
16
+ logger.error { error.message }
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :parser, :actions, :container
22
+
23
+ def perform configuration
24
+ case configuration
25
+ in action_config: Symbol => action then config action
26
+ in action_version: true then logger.info { "<%= configuration.project_label %> <%= configuration.project_version %>" }
27
+ else usage
28
+ end
29
+ end
30
+
31
+ def config(action) = actions.fetch(__method__).call(action)
32
+
33
+ def usage = logger.unknown(parser.to_s)
34
+
35
+ def logger = container[__method__]
36
+ end
37
+ end
38
+ <% end %>
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% namespace do %>
4
+ module Configuration
5
+ # Defines the content of the configuration for use throughout the gem.
6
+ Content = Struct.new(
7
+ :action_config,
8
+ :action_help,
9
+ :action_version,
10
+ keyword_init: true
11
+ ) do
12
+ def initialize *arguments
13
+ super
14
+ freeze
15
+ end
16
+ end
17
+ end
18
+ <% end %>
@@ -0,0 +1,33 @@
1
+ require "pathname"
2
+ require "refinements/hashes"
3
+ require "refinements/structs"
4
+ require "runcom"
5
+ require "yaml"
6
+
7
+ <% namespace do %>
8
+ module Configuration
9
+ # Represents the fully assembled Command Line Interface (CLI) configuration.
10
+ class Loader
11
+ using Refinements::Hashes
12
+ using Refinements::Structs
13
+
14
+ DEFAULTS = (YAML.load_file(Pathname(__dir__).join("defaults.yml")) || {}).freeze
15
+ CLIENT = Runcom::Config.new "<%= configuration.project_name %>/configuration.yml", defaults: DEFAULTS
16
+
17
+ def self.call = new.call
18
+
19
+ def self.with_defaults = new client: DEFAULTS
20
+
21
+ def initialize content: Content.new, client: CLIENT
22
+ @content = content
23
+ @client = client
24
+ end
25
+
26
+ def call = content.merge(**client.to_h.flatten_keys).freeze
27
+
28
+ private
29
+
30
+ attr_reader :content, :client
31
+ end
32
+ end
33
+ <% end %>
@@ -0,0 +1,35 @@
1
+ require "dry-container"
2
+ require "logger"
3
+ require "pastel"
4
+
5
+ <% namespace do %>
6
+ # Provides a global gem container for injection into other objects.
7
+ module Container
8
+ extend Dry::Container::Mixin
9
+
10
+ register(:configuration) { Configuration::Loader.call }
11
+ register(:colorizer) { Pastel.new enabled: $stdout.tty? }
12
+ register(:kernel) { Kernel }
13
+
14
+ register :log_colors do
15
+ {
16
+ "DEBUG" => self[:colorizer].white.detach,
17
+ "INFO" => self[:colorizer].green.detach,
18
+ "WARN" => self[:colorizer].yellow.detach,
19
+ "ERROR" => self[:colorizer].red.detach,
20
+ "FATAL" => self[:colorizer].white.bold.on_red.detach,
21
+ "ANY" => self[:colorizer].white.bold.detach
22
+ }
23
+ end
24
+
25
+ register :logger do
26
+ Logger.new $stdout,
27
+ level: Logger.const_get(ENV.fetch("LOG_LEVEL", "INFO")),
28
+ formatter: (
29
+ lambda do |severity, _at, _name, message|
30
+ self[:log_colors][severity].call "#{message}\n"
31
+ end
32
+ )
33
+ end
34
+ end
35
+ <% end %>
@@ -4,6 +4,6 @@
4
4
  NAME = "<%= config.dig(:gem, :name) %>"
5
5
  LABEL = "<%= config.dig(:gem, :label) %>"
6
6
  VERSION = "0.1.0"
7
- VERSION_LABEL = "#{LABEL} #{VERSION}"
7
+ VERSION_LABEL = "#{LABEL} #{VERSION}".freeze
8
8
  end
9
9
  <% end %>
@@ -0,0 +1,24 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe <%= configuration.project_class %>::CLI::Actions::Config do
4
+ subject(:action) { described_class.new }
5
+
6
+ include_context "with application container"
7
+
8
+ describe "#call" do
9
+ it "edits configuration" do
10
+ action.call :edit
11
+ expect(kernel).to have_received(:system).with(include("EDITOR"))
12
+ end
13
+
14
+ it "views configuration" do
15
+ action.call :view
16
+ expect(kernel).to have_received(:system).with(include("cat"))
17
+ end
18
+
19
+ it "logs invalid configuration" do
20
+ expectation = proc { action.call :bogus }
21
+ expect(&expectation).to output(/Invalid configuration selection: bogus./).to_stdout
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe <%= configuration.project_class %>::CLI::Parser do
4
+ subject(:parser) { described_class.new }
5
+
6
+ include_context "with application container"
7
+
8
+ describe "#call" do
9
+ it "answers hash with valid option" do
10
+ expect(parser.call(%w[--help])).to have_attributes(action_help: true)
11
+ end
12
+
13
+ it "fails with invalid option" do
14
+ expectation = proc { parser.call %w[--bogus] }
15
+ expect(&expectation).to raise_error(OptionParser::InvalidOption, /--bogus/)
16
+ end
17
+ end
18
+
19
+ describe "#to_s" do
20
+ it "answers usage" do
21
+ parser.call
22
+ expect(parser.to_s).to match(/.+USAGE.+/m)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,53 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe <%= configuration.project_class %>::CLI::Parsers::Core do
4
+ subject(:parser) { described_class.new configuration.dup }
5
+
6
+ include_context "with application container"
7
+
8
+ it_behaves_like "a parser"
9
+
10
+ describe "#call" do
11
+ it "answers config edit (short)" do
12
+ expect(parser.call(%w[-c edit])).to have_attributes(action_config: :edit)
13
+ end
14
+
15
+ it "answers config edit (long)" do
16
+ expect(parser.call(%w[--config edit])).to have_attributes(action_config: :edit)
17
+ end
18
+
19
+ it "answers config view (short)" do
20
+ expect(parser.call(%w[-c view])).to have_attributes(action_config: :view)
21
+ end
22
+
23
+ it "answers config view (long)" do
24
+ expect(parser.call(%w[--config view])).to have_attributes(action_config: :view)
25
+ end
26
+
27
+ it "fails with missing config action" do
28
+ expectation = proc { parser.call %w[--config] }
29
+ expect(&expectation).to raise_error(OptionParser::MissingArgument, /--config/)
30
+ end
31
+
32
+ it "fails with invalid config action" do
33
+ expectation = proc { parser.call %w[--config bogus] }
34
+ expect(&expectation).to raise_error(OptionParser::InvalidArgument, /bogus/)
35
+ end
36
+
37
+ it "answers version (short)" do
38
+ expect(parser.call(%w[-v])).to have_attributes(action_version: true)
39
+ end
40
+
41
+ it "answers version (long)" do
42
+ expect(parser.call(%w[--version])).to have_attributes(action_version: true)
43
+ end
44
+
45
+ it "enables help (short)" do
46
+ expect(parser.call(%w[-h])).to have_attributes(action_help: true)
47
+ end
48
+
49
+ it "enables help (long)" do
50
+ expect(parser.call(%w[--help])).to have_attributes(action_help: true)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,43 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe <%= configuration.project_class %>::CLI::Shell do
4
+ using Refinements::Pathnames
5
+
6
+ subject(:shell) { described_class.new actions: described_class::ACTIONS.merge(config:) }
7
+
8
+ include_context "with temporary directory"
9
+
10
+ let(:config) { instance_spy <%= configuration.project_class %>::CLI::Actions::Config }
11
+
12
+ describe "#call" do
13
+ it "edits configuration" do
14
+ shell.call %w[--config edit]
15
+ expect(config).to have_received(:call).with(:edit)
16
+ end
17
+
18
+ it "views configuration" do
19
+ shell.call %w[--config view]
20
+ expect(config).to have_received(:call).with(:view)
21
+ end
22
+
23
+ it "prints version" do
24
+ expectation = proc { shell.call %w[--version] }
25
+ expect(&expectation).to output(/<%= configuration.project_label %>\s\d+\.\d+\.\d+/).to_stdout
26
+ end
27
+
28
+ it "prints help (usage)" do
29
+ expectation = proc { shell.call %w[--help] }
30
+ expect(&expectation).to output(/<%= configuration.project_label %>.+USAGE.+/m).to_stdout
31
+ end
32
+
33
+ it "prints usage when no options are given" do
34
+ expectation = proc { shell.call }
35
+ expect(&expectation).to output(/<%= configuration.project_label %>.+USAGE.+/m).to_stdout
36
+ end
37
+
38
+ it "prints error with invalid option" do
39
+ expectation = proc { shell.call %w[--bogus] }
40
+ expect(&expectation).to output(/invalid option.+bogus/).to_stdout
41
+ end
42
+ end
43
+ end