# frozen_string_literal: true require "fileutils" require "pathname" module RubyNext module Commands class CoreExt < Base using RubyNext attr_reader :out_path, :min_version, :names, :list, :filter, :original_command alias list? list def run log "Select core extensions for Ruby v#{min_version}" \ "#{filter ? " and matching #{filter.inspect}" : ""}" matching_patches.then do |patches| next print_list(patches) if list? generate_core_ext(patches) end end def parse!(args) print_help = false @min_version = MIN_SUPPORTED_VERSION @original_command = "ruby-next core_ext #{args.join(" ")}" @names = [] @list = false @out_path = File.join(Dir.pwd, "core_ext.rb") optparser = base_parser do |opts| opts.banner = "Usage: ruby-next core_ext [options]" opts.on("-o", "--output=OUTPUT", "Specify output file or stdout (default: ./core_ext.rb)") do |val| @out_path = val end opts.on("-l", "--list", "List all available extensions") do @list = true end opts.on("--min-version=VERSION", "Specify the minimum Ruby version to support") do |val| @min_version = Gem::Version.new(val) end opts.on("-n", "--name=NAME", "Filter extensions by name") do |val| names << val end opts.on("-h", "--help", "Print help") do print_help = true end end optparser.parse!(args) if print_help $stdout.puts optparser.help exit 0 end @filter = /(#{names.join("|")})/i unless names.empty? end private def matching_patches RubyNext::Core.patches.extensions .values .flatten .select do |patch| next if min_version && Gem::Version.new(patch.version) <= min_version next if filter && !filter.match?(patch.name) true end end def print_list(patches) grouped_patches = patches.group_by(&:version).sort_by(&:first) grouped_patches.each do |(group, patches)| $stdout.puts "#{group} extensions:\n" $stdout.puts patches.sort_by(&:name).map { |patch| " - #{patch.name}" }.join("\n") $stdout.puts "\n" end end def generate_core_ext(patches) grouped_patches = patches.group_by(&:mod).sort_by { |(mod, patch)| mod.singleton_class? ? mod.inspect : mod.name } buffer = [] buffer << "# frozen_string_literal: true\n" buffer << generation_meta grouped_patches.each do |mod, patches| singleton = mod.singleton_class? extend_name = singleton ? patches.first.singleton.name : mod.name prepend_name = singleton ? "#{patches.first.singleton.name}.singleton_class" : mod.name prepended, extended = patches.partition(&:prepend?) prepended.map do |patch| name = "RubyNext::Core::#{patch.name}" buffer << <<-RUBY module #{name} #{indent_and_trim(patch.body)} end #{prepend_name}.prepend #{name} RUBY name end class_or_module = mod.is_a?(Class) ? "class" : "module" buffer << "#{class_or_module} #{extend_name}" buffer << " class << self" if singleton indent_size = singleton ? 4 : 2 buffer << extended.map do |patch| indent_and_trim(patch.body, indent_size) end.join("\n\n") buffer << " end" if singleton buffer << "end\n" end contents = buffer.join("\n") return $stdout.puts(contents) if out_path == "stdout" unless CLI.dry_run? FileUtils.mkdir_p File.dirname(out_path) File.write(out_path, contents) end log "Generated: #{out_path}" end def generation_meta <<-MSG # Generated by Ruby Next v#{RubyNext::VERSION} using the following command: # # #{original_command} # MSG end def indent_and_trim(src, size = 2) new_src = src.dup # indent code using spaces new_src.gsub!(/^/, " " * size) # remove empty lines new_src.gsub!(/^\s+$/, "") # remove traling blank lines new_src.delete_suffix!("\n") new_src end end end end