123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- # -*- coding: utf-8 -*-
- # -*- frozen_string_literal: true -*-
- require "optparse"
- require "optparse/uri"
- require "forwardable"
- require "erb"
- require "fileutils"
- # The script's main function.
- def scaffold_gem
- options = ScaffoldCLI.new.parse $argv
- details = ScaffoldDetails.new options
- Scaffold.gem details
- end
- class Scaffold
- # Path character wildcard.
- WILDCARD = "*"
- # Global access to path, and its in-between directories.
- GLOB = "**"
- # Gem file full name.
- STAND_IN_NAME = "_gem_name"
- # Gem file name that may be scoped, like plugins.
- STAND_IN_BASENAME = "_gem_basename"
- # Default binstub alternative platform
- ALT_PLATFORM_ID = "-alpine"
- # Test file identifier
- TEST_IDENTIFIER = "test_"
- # Scaffold main entry point. Assumes __dir__ is the working directory.
- def self.gem details
- scaffold = new details, directory: __dir__
- scaffold.
- fill_templates.
- rename_templates.
- rename_gem_files.
- clean_up
- end
- def initialize details, parser: ERB, directory:, template_extension: ".erb"
- @scaffold = details
- @parser = parser
- @directory = set_scaffold_dir(directory)
- @template_extension = template_extension
- end
- def templates
- Dir[directory + GLOB + dir_div + WILDCARD + template_extension]
- end
- def fill template
- content = @parser.new(File.read template).result binding
- File.open(template, "wb") { |f| f.write content }
- end
- def fill_templates
- templates.each { |t| fill t }
- self
- end
- def rename_templates
- templates.each do |template|
- new_name = template.sub(%r/#{template_extension}\z/, "")
- FileUtils.mv template, new_name
- end
- self
- end
- def rename_gem_files
- path = directory + GLOB + dir_div + WILDCARD
- files =
- Dir[path + STAND_IN_NAME + WILDCARD] + Dir[path + STAND_IN_BASENAME + WILDCARD]
- files.map { |file| FileUtils.mv file, new_filename(file), force: true }
- self
- end
- def clean_up
- remove_binstub unless @scaffold.bin
- relocate_gem if @scaffold.plugin_namespace
- remove_scaffolding
- end
- private
- attr_reader :directory, :template_extension
- def new_filename file
- old_name = File.basename(file)
- new_name = old_name.include?(STAND_IN_NAME) ? @scaffold.gem_name : @scaffold.gem_basename
- new_name.prepend(TEST_IDENTIFIER) if old_name.start_with?(TEST_IDENTIFIER)
- new_name = new_name + ALT_PLATFORM_ID if old_name.end_with?(ALT_PLATFORM_ID)
- File.dirname(file) + dir_div + new_name + File.extname(file)
- end
- def remove_binstub
- %W[#{directory}/bin #{directory}/test/bin].each { |dir| FileUtils.rm_r dir }
- end
- def relocate_gem
- entry_point = "lib/#{@scaffold.main_basename}"
- FileUtils.mkdir_p "#{directory}#{entry_point}"
- FileUtils.mkdir_p "#{directory}test/#{entry_point}"
- FileUtils.mv Dir["#{directory}lib/**/*"],
- "#{directory}#{entry_point}", force: true
- FileUtils.mv Dir["#{directory}test/lib/**/*"],
- "#{directory}test/#{entry_point}", force: true
- end
- def remove_scaffolding
- %W[
- #{directory}.git
- #{directory}.scaffold-ci.yml
- #{directory}ReadMe.org
- #{directory}ChangeLog.org
- #{directory}scaffold_gem.rb
- #{directory}test/test_scaffold_gem.rb
- ].each { |sf| FileUtils.rm_rf sf }
- end
- def set_scaffold_dir dir
- dir + dir_div
- end
- def dir_div
- ScaffoldDetails::DIR_DIV
- end
- end
- class ScaffoldDetails
- CAMEL_CASE_REGEX = /([a-z\d])([A-Z])/
- CONSTANT_RESOLUTION_OPERATOR = File::PATH_SEPARATOR * 2
- PLUGIN_IDENTIFIER = "-"
- DIR_DIV = File::SEPARATOR
- DIR_BACKTRACK = ".."
- extend Forwardable
- def_delegators :@options, :namespace, :author, :email, :repo, :license, :bin
- public :binding
- def initialize options
- @options = options
- @formatted_name = options.formatted_name
- end
- # Gem main namespace #=> SomeGem
- def main_namespace
- split_namespace.first
- end
- # Gem plugin namespace #=> SomePlugin
- def plugin_namespace
- _, scoped, _ = split_namespace
- scoped
- rescue
- nil
- end
- # The gem's namespace
- # Given SomeGem::SomePlugin #=> SomePlugin
- # Given SomeGem #=> SomeGem
- def gem_namespace
- plugin_namespace or main_namespace
- end
- # The gem's name as register in Rubygems.org
- # Given SomeGem::SomePlugin #=> some_gem-some_plugin
- def gem_name
- snake_case = '\1_\2'
- @gem_name ||= namespace.
- gsub(CAMEL_CASE_REGEX, snake_case).
- downcase.
- gsub CONSTANT_RESOLUTION_OPERATOR, PLUGIN_IDENTIFIER
- end
- # The gem's constant
- # Given SomeGem #=> SOME_GEM
- # Given SomeGem::SomePlugin #=> SOME_PLUGIN
- def gem_constant
- @constant ||= begin
- main, plugin, _ = gem_name_parts.map &:upcase
- plugin or main
- end
- end
- # Gem formatted name. As display in the ReadMe.
- # Given SomeGem::SomePlugin #=> Some Gem - Some Plugin
- # Can be setup to any valid string via the options given to a new scaffold_details.
- def formatted_name
- return @formatted_name unless @formatted_name.empty?
- words = '\1 \2'
- @formatted_name = namespace.
- gsub(CAMEL_CASE_REGEX, words).
- gsub CONSTANT_RESOLUTION_OPERATOR, formatted_plugin_identifier
- end
- # Gem main basename
- # Whether given SomeGem or SomeGem::SomePlugin #=> some_gem
- def main_basename
- gem_name_parts[0]
- end
- # Gem plugin basename
- # Given SomeGem::SomePlugin #=> some_plugin
- def plugin_basename
- gem_name_parts[1]
- end
- # Gem's basename
- # Given SomeGem::SomePlugin #=> some_plugin
- # Given SomeGem #=> some_gem
- def gem_basename
- plugin_basename or main_basename
- end
- # Gem's path
- # Given SomeGem #=> some_gem
- # Given SomeGem::SomePlugin #=> some_gem/some_plugin
- def gem_path
- plugin = DIR_DIV + plugin_basename if plugin_basename
- "#{main_basename}#{plugin}"
- end
- # Print the gem's require statement
- # Whether given SomeGem or SomeGem::SomePlugin #=> require "some_gem"
- def require_main
- %Q{require "#{main_basename}"}
- end
- # Print the gem's require statement as a plugin
- # Given SomeGem::SomePlugin #=> require "some_gem/some_plugin"
- def require_plugin
- return unless plugin_basename
- %Q{require "#{gem_path}"}
- end
- # Print minitest's config helper require statement from toplevel files
- # Given SomeGem #=> require_relative "./../_config/minitest"
- # Given SomeGem::SomePlugin #=> require_relative "./../../_config/minitest"
- def toplevel_require_minitest_config
- backtrack = plugin_namespace ? double_backtrack_dir : DIR_BACKTRACK
- require_minitest_config backtrack
- end
- # Print minitest's config helper require statement from sublevel files
- # Given SomeGem #=> require_relative "./../../_config/minitest"
- # Given SomeGem::SomePlugin #=> require_relative "./../../../_config/minitest"
- def sublevel_require_minitest_config
- triple_backtrack = double_backtrack_dir + backtrack_again
- backtrack = plugin_namespace ? triple_backtrack : double_backtrack_dir
- require_minitest_config backtrack
- end
- private
- def double_backtrack_dir
- DIR_BACKTRACK + backtrack_again
- end
- def backtrack_again
- DIR_DIV + DIR_BACKTRACK
- end
- def require_minitest_config backtrack
- %Q{require_relative "./#{backtrack}/_config/minitest"}
- end
- def gem_name_parts
- @gem_name_parts ||= gem_name.split PLUGIN_IDENTIFIER
- end
- def formatted_plugin_identifier
- " #{PLUGIN_IDENTIFIER} "
- end
- def split_namespace
- namespace.split CONSTANT_RESOLUTION_OPERATOR
- end
- end
- class ScaffoldCLI
- # The scaffold CLI options. Can be initialized using keywords.
- # Missing members default to nil, except for bin, falling back to +false+, and
- # formatted_name, to an empty string.
- ScaffoldOptions = Struct.new :namespace, :author, :email, :repo, :license,
- :bin, :formatted_name, keyword_init: true do
- def initialize namespace:nil, author:nil, email:nil, repo:nil, license:nil,
- bin:nil, formatted_name:nil
- super
- self.bin ||= false
- self.formatted_name ||= ""
- end
- end
- def initialize options=ScaffoldOptions.new, notice=$stdout, err=$stderr
- @notice = notice
- @options = options
- @err = err
- end
- def parse argv=$argv, parser: OptionParser
- cli = build_argv_parser parser
- cli.parse!(*argv)
- check_mandatory_values options, cli
- options.freeze
- end
- private # :nodoc:
- attr_reader :options, :notice, :err
- def check_mandatory_values opts, cli
- all_mandatory_values =
- opts.namespace && opts.author && opts.email && opts.repo && opts.license
- unless all_mandatory_values
- err.puts "Must provide all mandatory options."
- show_help cli
- exit_cli status: 2
- end
- end
- def build_argv_parser parser
- parser.new do |p|
- set_formatted_name p
- set_bin p
- set_license p
- set_repo p
- set_email p
- set_author p
- set_namespace p
- display_help p
- end
- end
- def set_formatted_name cli
- desc = "Add unsupported name style convention"
- cli.on("-f", "--formatted-name=NAME", String, desc) { |n| options.formatted_name = n }
- end
- def set_bin cli
- desc = "Add binstub"
- cli.on("-b", "--bin", TrueClass, desc) { |b| options.bin = b }
- end
- def set_license cli
- desc = "Set mandatory license identifier. Details https://spdx.org/licenses"
- cli.on("-l", "--license=LICENSE", String, desc) { |l| options.license = l }
- end
- def set_repo cli
- desc = "Set mandatory gem repo URL"
- cli.on("-r", "--repo=REPO", URI, desc) { |r| options.repo = r.to_s }
- end
- def set_email cli
- desc = "Set mandatory gem contact email"
- # regex from https://www.owasp.org/index.php/OWASP_Validation_Regex_Repository
- regex = /\A[a-zA-Z0-9_+&*-]+(?:\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,7}\z/
- cli.on("-e", "--email=EMAIL", regex, desc) { |e| options.email = e }
- end
- def set_author cli
- desc = "Set mandatory gem author"
- cli.on("-a", "--author=AUTHOR", String, desc) { |a| options.author = a }
- end
- def set_namespace cli
- desc = "Set mandatory gem namespace"
- cli.on("-n", "--namespace=NAMESPACE", String, desc) { |n| options.namespace = n }
- end
- def display_help cli
- cli.on_tail("-h", "--help", "Show this message") {
- show_help cli
- exit_cli
- }
- end
- def show_help cli
- notice.puts cli
- end
- def exit_cli status: 0
- exit status unless ENV["GEM_SCAFFOLD_ENV"] == "test"
- end
- end
- # The ruby interpreter scaffold_gem when the script is run as command line program
- if $PROGRAM_NAME == __FILE__
- scaffold_gem
- end
|