create-ruby-gem 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +150 -0
- data/exe/create-ruby-gem +13 -0
- data/lib/create_ruby_gem/cli.rb +360 -0
- data/lib/create_ruby_gem/command_builder.rb +54 -0
- data/lib/create_ruby_gem/compatibility/matrix.rb +118 -0
- data/lib/create_ruby_gem/config/store.rb +125 -0
- data/lib/create_ruby_gem/detection/bundler_defaults.rb +88 -0
- data/lib/create_ruby_gem/detection/bundler_version.rb +31 -0
- data/lib/create_ruby_gem/detection/runtime.rb +33 -0
- data/lib/create_ruby_gem/error.rb +15 -0
- data/lib/create_ruby_gem/options/catalog.rb +49 -0
- data/lib/create_ruby_gem/options/validator.rb +102 -0
- data/lib/create_ruby_gem/runner.rb +39 -0
- data/lib/create_ruby_gem/ui/back_navigation_patch.rb +41 -0
- data/lib/create_ruby_gem/ui/palette.rb +73 -0
- data/lib/create_ruby_gem/ui/prompter.rb +77 -0
- data/lib/create_ruby_gem/version.rb +6 -0
- data/lib/create_ruby_gem/wizard.rb +339 -0
- data/lib/create_ruby_gem.rb +29 -0
- metadata +96 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CreateRubyGem
|
|
4
|
+
module Compatibility
|
|
5
|
+
# Static lookup table mapping Bundler version ranges to the +bundle gem+
|
|
6
|
+
# options each range supports.
|
|
7
|
+
#
|
|
8
|
+
# This is the single source of truth for what each Bundler version
|
|
9
|
+
# can do. The wizard, validator, and builder all consult it.
|
|
10
|
+
#
|
|
11
|
+
# @example Look up the entry for a specific Bundler version
|
|
12
|
+
# entry = Matrix.for('3.1.0')
|
|
13
|
+
# entry.supports_option?(:linter) #=> true
|
|
14
|
+
class Matrix
|
|
15
|
+
# A single row in the compatibility table.
|
|
16
|
+
#
|
|
17
|
+
# @!attribute [r] requirement
|
|
18
|
+
# @return [Gem::Requirement] Bundler version range this entry covers
|
|
19
|
+
# @!attribute [r] supported_options
|
|
20
|
+
# @return [Hash{Symbol => Array<String>, nil}] option keys to allowed
|
|
21
|
+
# values (+nil+ means any boolean/toggle value is accepted)
|
|
22
|
+
Entry = Struct.new(:requirement, :supported_options, keyword_init: true) do
|
|
23
|
+
# @param bundler_version [Gem::Version] version to test
|
|
24
|
+
# @return [Boolean]
|
|
25
|
+
def match?(bundler_version)
|
|
26
|
+
requirement.satisfied_by?(bundler_version)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param option_key [Symbol, String]
|
|
30
|
+
# @return [Boolean]
|
|
31
|
+
def supports_option?(option_key)
|
|
32
|
+
supported_options.key?(option_key.to_sym)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param option_key [Symbol, String]
|
|
36
|
+
# @return [Array<String>, nil] allowed values, or nil for toggles/flags
|
|
37
|
+
def allowed_values(option_key)
|
|
38
|
+
supported_options.fetch(option_key.to_sym)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [Array<Entry>] all known Bundler compatibility entries
|
|
43
|
+
TABLE = [
|
|
44
|
+
Entry.new(
|
|
45
|
+
requirement: Gem::Requirement.new('>= 2.4', '< 3.0'),
|
|
46
|
+
supported_options: {
|
|
47
|
+
exe: nil,
|
|
48
|
+
coc: nil,
|
|
49
|
+
ext: %w[c],
|
|
50
|
+
git: nil,
|
|
51
|
+
github_username: nil,
|
|
52
|
+
mit: nil,
|
|
53
|
+
test: %w[minitest rspec test-unit],
|
|
54
|
+
ci: %w[circle github gitlab],
|
|
55
|
+
edit: nil,
|
|
56
|
+
bundle_install: nil
|
|
57
|
+
}
|
|
58
|
+
),
|
|
59
|
+
Entry.new(
|
|
60
|
+
requirement: Gem::Requirement.new('>= 3.0', '< 4.0'),
|
|
61
|
+
supported_options: {
|
|
62
|
+
exe: nil,
|
|
63
|
+
coc: nil,
|
|
64
|
+
changelog: nil,
|
|
65
|
+
ext: %w[c],
|
|
66
|
+
git: nil,
|
|
67
|
+
github_username: nil,
|
|
68
|
+
mit: nil,
|
|
69
|
+
test: %w[minitest rspec test-unit],
|
|
70
|
+
ci: %w[circle github gitlab],
|
|
71
|
+
linter: %w[rubocop standard],
|
|
72
|
+
edit: nil,
|
|
73
|
+
bundle_install: nil
|
|
74
|
+
}
|
|
75
|
+
),
|
|
76
|
+
Entry.new(
|
|
77
|
+
requirement: Gem::Requirement.new('>= 4.0', '< 5.0'),
|
|
78
|
+
supported_options: {
|
|
79
|
+
exe: nil,
|
|
80
|
+
coc: nil,
|
|
81
|
+
changelog: nil,
|
|
82
|
+
ext: %w[c go rust],
|
|
83
|
+
git: nil,
|
|
84
|
+
github_username: nil,
|
|
85
|
+
mit: nil,
|
|
86
|
+
test: %w[minitest rspec test-unit],
|
|
87
|
+
ci: %w[circle github gitlab],
|
|
88
|
+
linter: %w[rubocop standard],
|
|
89
|
+
edit: nil,
|
|
90
|
+
bundle_install: nil
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
].freeze
|
|
94
|
+
|
|
95
|
+
# Returns human-readable version range strings for all entries.
|
|
96
|
+
#
|
|
97
|
+
# @return [Array<String>]
|
|
98
|
+
def self.supported_ranges
|
|
99
|
+
TABLE.map { |entry| entry.requirement.requirements.map(&:join).join(', ') }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Finds the compatibility entry for the given Bundler version.
|
|
103
|
+
#
|
|
104
|
+
# @param bundler_version [Gem::Version, String] the Bundler version
|
|
105
|
+
# @return [Entry]
|
|
106
|
+
# @raise [UnsupportedBundlerVersionError] if no entry matches
|
|
107
|
+
def self.for(bundler_version)
|
|
108
|
+
version = Gem::Version.new(bundler_version.to_s)
|
|
109
|
+
entry = TABLE.find { |candidate| candidate.match?(version) }
|
|
110
|
+
return entry if entry
|
|
111
|
+
|
|
112
|
+
message = "Unsupported bundler version: #{version}. "
|
|
113
|
+
message += "Supported ranges: #{supported_ranges.join(' | ')}"
|
|
114
|
+
raise UnsupportedBundlerVersionError, message
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
require 'yaml'
|
|
6
|
+
|
|
7
|
+
module CreateRubyGem
|
|
8
|
+
module Config
|
|
9
|
+
# YAML persistence for presets and last-used options.
|
|
10
|
+
#
|
|
11
|
+
# Stores configuration at +~/.config/create-ruby-gem/config.yml+ (or
|
|
12
|
+
# +$XDG_CONFIG_HOME/create-ruby-gem/config.yml+). Writes are atomic
|
|
13
|
+
# via +Tempfile+ + rename.
|
|
14
|
+
class Store
|
|
15
|
+
# @return [Integer] current config file schema version
|
|
16
|
+
SCHEMA_VERSION = 1
|
|
17
|
+
|
|
18
|
+
# @param path [String, nil] override the default config file path
|
|
19
|
+
def initialize(path: nil)
|
|
20
|
+
@path = path || default_path
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [String] absolute path to the config file
|
|
24
|
+
attr_reader :path
|
|
25
|
+
|
|
26
|
+
# Returns the last-used option hash (empty hash if none saved).
|
|
27
|
+
#
|
|
28
|
+
# @return [Hash{String => Object}]
|
|
29
|
+
def last_used
|
|
30
|
+
data.fetch('last_used')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Persists the given options as last-used.
|
|
34
|
+
#
|
|
35
|
+
# @param options [Hash{Symbol => Object}]
|
|
36
|
+
# @return [void]
|
|
37
|
+
def save_last_used(options)
|
|
38
|
+
payload = data
|
|
39
|
+
payload['last_used'] = stringify_keys(options)
|
|
40
|
+
write(payload)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns a preset by name, or +nil+ if it does not exist.
|
|
44
|
+
#
|
|
45
|
+
# @param name [String]
|
|
46
|
+
# @return [Hash{String => Object}, nil]
|
|
47
|
+
def preset(name)
|
|
48
|
+
data.fetch('presets').fetch(name.to_s, nil)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returns all preset names sorted alphabetically.
|
|
52
|
+
#
|
|
53
|
+
# @return [Array<String>]
|
|
54
|
+
def preset_names
|
|
55
|
+
data.fetch('presets').keys.sort
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Saves a named preset.
|
|
59
|
+
#
|
|
60
|
+
# @param name [String]
|
|
61
|
+
# @param options [Hash{Symbol => Object}]
|
|
62
|
+
# @return [void]
|
|
63
|
+
def save_preset(name, options)
|
|
64
|
+
payload = data
|
|
65
|
+
payload.fetch('presets')[name.to_s] = stringify_keys(options)
|
|
66
|
+
write(payload)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Deletes a named preset (no-op if it does not exist).
|
|
70
|
+
#
|
|
71
|
+
# @param name [String]
|
|
72
|
+
# @return [void]
|
|
73
|
+
def delete_preset(name)
|
|
74
|
+
payload = data
|
|
75
|
+
payload.fetch('presets').delete(name.to_s)
|
|
76
|
+
write(payload)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# @return [Hash{String => Object}]
|
|
82
|
+
def data
|
|
83
|
+
raw = load
|
|
84
|
+
{
|
|
85
|
+
'version' => raw.fetch('version', SCHEMA_VERSION),
|
|
86
|
+
'last_used' => raw.fetch('last_used', {}),
|
|
87
|
+
'presets' => raw.fetch('presets', {})
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @return [Hash]
|
|
92
|
+
# @raise [ConfigError] if YAML is malformed
|
|
93
|
+
def load
|
|
94
|
+
return {} unless File.file?(path)
|
|
95
|
+
|
|
96
|
+
YAML.safe_load_file(path, aliases: false) || {}
|
|
97
|
+
rescue Psych::SyntaxError => e
|
|
98
|
+
raise ConfigError, "Invalid config file at #{path}: #{e.message}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @param payload [Hash]
|
|
102
|
+
# @return [void]
|
|
103
|
+
def write(payload)
|
|
104
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
105
|
+
Tempfile.create(['create-ruby-gem', '.yml'], File.dirname(path)) do |tmp|
|
|
106
|
+
tmp.write(YAML.dump(payload))
|
|
107
|
+
tmp.flush
|
|
108
|
+
File.rename(tmp.path, path)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [String]
|
|
113
|
+
def default_path
|
|
114
|
+
config_home = ENV.fetch('XDG_CONFIG_HOME', File.join(Dir.home, '.config'))
|
|
115
|
+
File.join(config_home, 'create-ruby-gem', 'config.yml')
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @param hash [Hash{Symbol => Object}]
|
|
119
|
+
# @return [Hash{String => Object}]
|
|
120
|
+
def stringify_keys(hash)
|
|
121
|
+
hash.transform_keys(&:to_s)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CreateRubyGem
|
|
4
|
+
module Detection
|
|
5
|
+
# Reads Bundler's own default settings (from +~/.bundle/config+ or env)
|
|
6
|
+
# to use as initial defaults in the wizard.
|
|
7
|
+
class BundlerDefaults
|
|
8
|
+
# Sentinel for "no settings argument provided".
|
|
9
|
+
UNSET = Object.new.freeze
|
|
10
|
+
|
|
11
|
+
# Fallback defaults used when Bundler settings are unavailable.
|
|
12
|
+
#
|
|
13
|
+
# @return [Hash{Symbol => Object}]
|
|
14
|
+
FALLBACKS = {
|
|
15
|
+
exe: false,
|
|
16
|
+
coc: nil,
|
|
17
|
+
changelog: nil,
|
|
18
|
+
ext: false,
|
|
19
|
+
git: true,
|
|
20
|
+
github_username: nil,
|
|
21
|
+
mit: nil,
|
|
22
|
+
test: nil,
|
|
23
|
+
ci: nil,
|
|
24
|
+
linter: nil,
|
|
25
|
+
edit: nil,
|
|
26
|
+
bundle_install: false
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
# Maps option keys to their Bundler settings keys.
|
|
30
|
+
#
|
|
31
|
+
# @return [Hash{Symbol => String}]
|
|
32
|
+
KEY_MAP = {
|
|
33
|
+
coc: 'gem.coc',
|
|
34
|
+
changelog: 'gem.changelog',
|
|
35
|
+
ext: 'gem.ext',
|
|
36
|
+
git: 'gem.git',
|
|
37
|
+
github_username: 'gem.github_username',
|
|
38
|
+
mit: 'gem.mit',
|
|
39
|
+
test: 'gem.test',
|
|
40
|
+
ci: 'gem.ci',
|
|
41
|
+
linter: 'gem.linter'
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
# @param settings [#[], nil] Bundler settings object (or nil to auto-detect)
|
|
45
|
+
def initialize(settings: UNSET)
|
|
46
|
+
@settings = settings.equal?(UNSET) ? default_settings : settings
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns a hash of default values derived from Bundler settings.
|
|
50
|
+
#
|
|
51
|
+
# @return [Hash{Symbol => Object}]
|
|
52
|
+
def detect
|
|
53
|
+
defaults = FALLBACKS.dup
|
|
54
|
+
return defaults unless @settings
|
|
55
|
+
|
|
56
|
+
KEY_MAP.each do |option_key, bundler_key|
|
|
57
|
+
value = normalize(@settings[bundler_key])
|
|
58
|
+
defaults[option_key] = value unless value.nil?
|
|
59
|
+
end
|
|
60
|
+
defaults
|
|
61
|
+
rescue StandardError
|
|
62
|
+
FALLBACKS.dup
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# @return [Bundler::Settings, nil]
|
|
68
|
+
def default_settings
|
|
69
|
+
return nil unless defined?(::Bundler)
|
|
70
|
+
|
|
71
|
+
::Bundler.settings
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @param value [String, Object, nil]
|
|
75
|
+
# @return [Boolean, String, nil]
|
|
76
|
+
def normalize(value)
|
|
77
|
+
case value
|
|
78
|
+
when 'true'
|
|
79
|
+
true
|
|
80
|
+
when 'false'
|
|
81
|
+
false
|
|
82
|
+
else
|
|
83
|
+
value
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CreateRubyGem
|
|
4
|
+
module Detection
|
|
5
|
+
# Detects the installed Bundler version by running +bundle --version+.
|
|
6
|
+
class BundlerVersion
|
|
7
|
+
# @return [Regexp] pattern to extract a version string
|
|
8
|
+
VERSION_PATTERN = /(\d+\.\d+\.\d+)/
|
|
9
|
+
|
|
10
|
+
# @param bundle_command [String] path or name of the bundle executable
|
|
11
|
+
def initialize(bundle_command: 'bundle')
|
|
12
|
+
@bundle_command = bundle_command
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Runs +bundle --version+ and parses the result.
|
|
16
|
+
#
|
|
17
|
+
# @return [Gem::Version]
|
|
18
|
+
# @raise [UnsupportedBundlerVersionError] if the version cannot be parsed
|
|
19
|
+
# or the executable is not found
|
|
20
|
+
def detect!
|
|
21
|
+
output = IO.popen([@bundle_command, '--version'], err: %i[child out], &:read)
|
|
22
|
+
version = output[VERSION_PATTERN, 1]
|
|
23
|
+
return Gem::Version.new(version) if version
|
|
24
|
+
|
|
25
|
+
raise UnsupportedBundlerVersionError, "Cannot parse bundler version from: #{output.inspect}"
|
|
26
|
+
rescue Errno::ENOENT
|
|
27
|
+
raise UnsupportedBundlerVersionError, "Bundler executable not found: #{@bundle_command}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CreateRubyGem
|
|
4
|
+
module Detection
|
|
5
|
+
# Holds detected Ruby, RubyGems, and Bundler versions.
|
|
6
|
+
#
|
|
7
|
+
# @!attribute [r] ruby
|
|
8
|
+
# @return [Gem::Version]
|
|
9
|
+
# @!attribute [r] rubygems
|
|
10
|
+
# @return [Gem::Version]
|
|
11
|
+
# @!attribute [r] bundler
|
|
12
|
+
# @return [Gem::Version]
|
|
13
|
+
RuntimeInfo = Struct.new(:ruby, :rubygems, :bundler, keyword_init: true)
|
|
14
|
+
|
|
15
|
+
# Detects the current Ruby, RubyGems, and Bundler versions.
|
|
16
|
+
class Runtime
|
|
17
|
+
# @param bundler_detector [Detection::BundlerVersion]
|
|
18
|
+
def initialize(bundler_detector: BundlerVersion.new)
|
|
19
|
+
@bundler_detector = bundler_detector
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [RuntimeInfo]
|
|
23
|
+
# @raise [UnsupportedBundlerVersionError] if Bundler cannot be detected
|
|
24
|
+
def detect!
|
|
25
|
+
RuntimeInfo.new(
|
|
26
|
+
ruby: Gem::Version.new(RUBY_VERSION),
|
|
27
|
+
rubygems: Gem::Version.new(Gem::VERSION),
|
|
28
|
+
bundler: @bundler_detector.detect!
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CreateRubyGem
|
|
4
|
+
# Base error for all create-ruby-gem failures.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when the config file is corrupt or unreadable.
|
|
8
|
+
class ConfigError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when CLI flags or option values are invalid.
|
|
11
|
+
class ValidationError < Error; end
|
|
12
|
+
|
|
13
|
+
# Raised when the detected Bundler version is outside all known ranges.
|
|
14
|
+
class UnsupportedBundlerVersionError < Error; end
|
|
15
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CreateRubyGem
|
|
4
|
+
module Options
|
|
5
|
+
# Registry of every +bundle gem+ option create-ruby-gem knows about.
|
|
6
|
+
#
|
|
7
|
+
# Each entry in {DEFINITIONS} describes the option's type and CLI flags.
|
|
8
|
+
# {ORDER} controls the sequence in which the wizard presents options.
|
|
9
|
+
#
|
|
10
|
+
# @see Options::Validator
|
|
11
|
+
# @see Wizard::Session
|
|
12
|
+
module Catalog
|
|
13
|
+
# Option definitions keyed by symbolic name.
|
|
14
|
+
#
|
|
15
|
+
# Each value is a Hash with +:type+ and the relevant flag keys
|
|
16
|
+
# (+:on+/+:off+ for toggles, +:flag+ for enums/strings, etc.).
|
|
17
|
+
#
|
|
18
|
+
# @return [Hash{Symbol => Hash}]
|
|
19
|
+
DEFINITIONS = {
|
|
20
|
+
exe: { type: :toggle, on: '--exe', off: '--no-exe' },
|
|
21
|
+
coc: { type: :toggle, on: '--coc', off: '--no-coc' },
|
|
22
|
+
changelog: { type: :toggle, on: '--changelog', off: '--no-changelog' },
|
|
23
|
+
ext: { type: :enum, flag: '--ext', none: '--no-ext', values: %w[c go rust] },
|
|
24
|
+
git: { type: :flag, on: '--git' },
|
|
25
|
+
github_username: { type: :string, flag: '--github-username' },
|
|
26
|
+
mit: { type: :toggle, on: '--mit', off: '--no-mit' },
|
|
27
|
+
test: { type: :enum, flag: '--test', none: '--no-test', values: %w[minitest rspec test-unit] },
|
|
28
|
+
ci: { type: :enum, flag: '--ci', none: '--no-ci', values: %w[circle github gitlab] },
|
|
29
|
+
linter: { type: :enum, flag: '--linter', none: '--no-linter', values: %w[rubocop standard] },
|
|
30
|
+
edit: { type: :string, flag: '--edit' },
|
|
31
|
+
bundle_install: { type: :toggle, on: '--bundle', off: '--no-bundle' }
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
# Wizard step order — matches the sequence users see.
|
|
35
|
+
#
|
|
36
|
+
# @return [Array<Symbol>]
|
|
37
|
+
ORDER = DEFINITIONS.keys.freeze
|
|
38
|
+
|
|
39
|
+
# Fetches the definition for a given option key.
|
|
40
|
+
#
|
|
41
|
+
# @param key [Symbol, String]
|
|
42
|
+
# @return [Hash] the option definition
|
|
43
|
+
# @raise [KeyError] if the key is unknown
|
|
44
|
+
def self.fetch(key)
|
|
45
|
+
DEFINITIONS.fetch(key.to_sym)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CreateRubyGem
|
|
4
|
+
module Options
|
|
5
|
+
# Validates user-selected options against the compatibility entry
|
|
6
|
+
# for the detected Bundler version.
|
|
7
|
+
#
|
|
8
|
+
# Checks that: the gem name is valid, every option key is known,
|
|
9
|
+
# every option is supported by this Bundler version, and every
|
|
10
|
+
# value is valid for the option's type and allowed values.
|
|
11
|
+
class Validator
|
|
12
|
+
# @return [Regexp] pattern for valid gem names
|
|
13
|
+
GEM_NAME_PATTERN = /\A[a-zA-Z][a-zA-Z0-9_-]*\z/
|
|
14
|
+
|
|
15
|
+
# @param compatibility_entry [Compatibility::Matrix::Entry]
|
|
16
|
+
def initialize(compatibility_entry)
|
|
17
|
+
@compatibility_entry = compatibility_entry
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Validates the gem name and all options.
|
|
21
|
+
#
|
|
22
|
+
# @param gem_name [String]
|
|
23
|
+
# @param options [Hash{Symbol => Object}]
|
|
24
|
+
# @return [true]
|
|
25
|
+
# @raise [ValidationError] if any validation fails
|
|
26
|
+
def validate!(gem_name:, options:) # rubocop:disable Naming/PredicateMethod
|
|
27
|
+
validate_gem_name!(gem_name)
|
|
28
|
+
|
|
29
|
+
options.each do |key, value|
|
|
30
|
+
validate_option_key!(key)
|
|
31
|
+
validate_supported_option!(key)
|
|
32
|
+
validate_value!(key, value)
|
|
33
|
+
validate_supported_value!(key, value)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# @param gem_name [String]
|
|
42
|
+
# @raise [ValidationError]
|
|
43
|
+
# @return [void]
|
|
44
|
+
def validate_gem_name!(gem_name)
|
|
45
|
+
return if gem_name.is_a?(String) && gem_name.match?(GEM_NAME_PATTERN)
|
|
46
|
+
|
|
47
|
+
raise ValidationError, "Invalid gem name: #{gem_name.inspect}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param key [Symbol]
|
|
51
|
+
# @raise [ValidationError]
|
|
52
|
+
# @return [void]
|
|
53
|
+
def validate_option_key!(key)
|
|
54
|
+
return if Catalog::DEFINITIONS.key?(key.to_sym)
|
|
55
|
+
|
|
56
|
+
raise ValidationError, "Unknown option: #{key}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @param key [Symbol]
|
|
60
|
+
# @raise [ValidationError]
|
|
61
|
+
# @return [void]
|
|
62
|
+
def validate_supported_option!(key)
|
|
63
|
+
return if @compatibility_entry.supports_option?(key)
|
|
64
|
+
|
|
65
|
+
raise ValidationError, "Option #{key} is not supported by this bundler version"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @param key [Symbol]
|
|
69
|
+
# @param value [Object]
|
|
70
|
+
# @raise [ValidationError]
|
|
71
|
+
# @return [void]
|
|
72
|
+
def validate_value!(key, value)
|
|
73
|
+
definition = Catalog.fetch(key)
|
|
74
|
+
case definition[:type]
|
|
75
|
+
when :toggle
|
|
76
|
+
return if value.nil? || value == true || value == false
|
|
77
|
+
when :flag
|
|
78
|
+
return if value.nil? || value == true
|
|
79
|
+
when :enum
|
|
80
|
+
return if value.nil? || value == false || definition[:values].include?(value)
|
|
81
|
+
when :string
|
|
82
|
+
return if value.nil? || value.is_a?(String)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
raise ValidationError, "Invalid value for #{key}: #{value.inspect}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @param key [Symbol]
|
|
89
|
+
# @param value [Object]
|
|
90
|
+
# @raise [ValidationError]
|
|
91
|
+
# @return [void]
|
|
92
|
+
def validate_supported_value!(key, value)
|
|
93
|
+
return if value.nil? || value == true || value == false
|
|
94
|
+
|
|
95
|
+
supported_values = @compatibility_entry.allowed_values(key)
|
|
96
|
+
return if supported_values.nil? || supported_values.include?(value)
|
|
97
|
+
|
|
98
|
+
raise ValidationError, "Value #{value.inspect} for #{key} is not supported by this bundler version"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cli/kit'
|
|
4
|
+
require 'shellwords'
|
|
5
|
+
|
|
6
|
+
module CreateRubyGem
|
|
7
|
+
# Executes the assembled +bundle gem+ command via the shell.
|
|
8
|
+
#
|
|
9
|
+
# Supports +--dry-run+ mode, which prints the command instead of running it.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# Runner.new.run!(['bundle', 'gem', 'my_gem', '--exe'])
|
|
13
|
+
class Runner
|
|
14
|
+
# @param out [IO] output stream for dry-run printing
|
|
15
|
+
# @param system_runner [#call, nil] callable that executes a shell command
|
|
16
|
+
def initialize(out: $stdout, system_runner: nil)
|
|
17
|
+
@out = out
|
|
18
|
+
@system_runner = system_runner || ->(*command) { ::CLI::Kit::System.system(*command) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Executes the command or prints it in dry-run mode.
|
|
22
|
+
#
|
|
23
|
+
# @param command [Array<String>] the command to execute
|
|
24
|
+
# @param dry_run [Boolean] when true, prints instead of executing
|
|
25
|
+
# @return [true] on success
|
|
26
|
+
# @raise [Error] if the command exits with a non-zero status
|
|
27
|
+
def run!(command, dry_run: false)
|
|
28
|
+
if dry_run
|
|
29
|
+
@out.puts(command.join(' '))
|
|
30
|
+
return true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
status = @system_runner.call(*command)
|
|
34
|
+
return true if status.respond_to?(:success?) && status.success?
|
|
35
|
+
|
|
36
|
+
raise Error, "Command failed: #{command.shelljoin}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CreateRubyGem
|
|
4
|
+
module UI
|
|
5
|
+
# Raised when the user presses Ctrl+B during a prompt.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
class BackKeyPressed < StandardError; end
|
|
9
|
+
|
|
10
|
+
# Monkey-patches +CLI::UI::Prompt+ to intercept Ctrl+B for back-navigation.
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
module BackNavigationPatch
|
|
14
|
+
# ASCII code for Ctrl+B.
|
|
15
|
+
CTRL_B = "\u0002"
|
|
16
|
+
|
|
17
|
+
# Patch module prepended onto +CLI::UI::Prompt.singleton_class+.
|
|
18
|
+
module PromptReadCharPatch
|
|
19
|
+
# @raise [BackKeyPressed] when the user presses Ctrl+B
|
|
20
|
+
# @return [String] the character read
|
|
21
|
+
def read_char
|
|
22
|
+
char = super
|
|
23
|
+
|
|
24
|
+
raise BackKeyPressed if char == BackNavigationPatch::CTRL_B
|
|
25
|
+
|
|
26
|
+
char
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Prepends the Ctrl+B patch onto +CLI::UI::Prompt+ (idempotent).
|
|
31
|
+
#
|
|
32
|
+
# @return [void]
|
|
33
|
+
def self.apply!
|
|
34
|
+
singleton = ::CLI::UI::Prompt.singleton_class
|
|
35
|
+
return if singleton.ancestors.include?(PromptReadCharPatch)
|
|
36
|
+
|
|
37
|
+
singleton.prepend(PromptReadCharPatch)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|