create-ruby-app 1.1.0 → 1.3.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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/CLAUDE.md +74 -0
  4. data/CODE_REVIEW.md +1659 -0
  5. data/LICENSE +13 -21
  6. data/README.md +5 -5
  7. data/REFACTORING_PLAN.md +543 -0
  8. data/bin/create-ruby-app +1 -3
  9. data/lib/create_ruby_app/actions/create_directories.rb +10 -2
  10. data/lib/create_ruby_app/actions/generate_files.rb +7 -4
  11. data/lib/create_ruby_app/actions/install_gems.rb +10 -2
  12. data/lib/create_ruby_app/actions/make_script_executable.rb +10 -2
  13. data/lib/create_ruby_app/actions/set_ruby_implementation.rb +52 -0
  14. data/lib/create_ruby_app/app.rb +9 -8
  15. data/lib/create_ruby_app/cli.rb +58 -41
  16. data/lib/create_ruby_app/templates/Gemfile.erb +1 -3
  17. data/lib/create_ruby_app/templates/lib_file.erb +0 -2
  18. data/lib/create_ruby_app/templates/script_file.erb +0 -2
  19. data/lib/create_ruby_app/templates/spec_helper.erb +0 -2
  20. data/lib/create_ruby_app/version.rb +1 -3
  21. data/lib/create_ruby_app.rb +1 -3
  22. data/spec/integration/app_creation_spec.rb +170 -0
  23. data/spec/lib/create_ruby_app/actions/create_directories_spec.rb +1 -3
  24. data/spec/lib/create_ruby_app/actions/generate_files_spec.rb +13 -20
  25. data/spec/lib/create_ruby_app/actions/install_gems_spec.rb +1 -3
  26. data/spec/lib/create_ruby_app/actions/make_script_executable_spec.rb +1 -3
  27. data/spec/lib/create_ruby_app/actions/set_ruby_implementation_spec.rb +194 -0
  28. data/spec/lib/create_ruby_app/app_spec.rb +4 -4
  29. data/spec/lib/create_ruby_app/cli_spec.rb +112 -0
  30. data/spec/spec_helper.rb +6 -2
  31. metadata +52 -20
  32. data/lib/create_ruby_app/actions/null_action.rb +0 -9
@@ -1,13 +1,21 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "fileutils"
4
2
 
5
3
  module CreateRubyApp
6
4
  module Actions
7
5
  class MakeScriptExecutable
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
8
10
  def self.call(app)
11
+ new(app).call
12
+ end
13
+
14
+ def call
9
15
  FileUtils.chmod("+x", "#{app.name}/bin/#{app.name}")
10
16
  end
17
+
18
+ attr_reader :app
11
19
  end
12
20
  end
13
21
  end
@@ -0,0 +1,52 @@
1
+ require "open3"
2
+
3
+ module CreateRubyApp
4
+ module Actions
5
+ class NoRubyImplementationFoundError < StandardError; end
6
+
7
+ class SetRubyImplementation
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ def self.call(app)
13
+ new(app).call
14
+ end
15
+
16
+ def call
17
+ return app if app.version
18
+
19
+ RUBY_IMPLEMENTATIONS.find do |ruby_implementation|
20
+ stdout, status = fetch_ruby_implementation(ruby_implementation)
21
+
22
+ if status.success? && !stdout.empty?
23
+ app.version = transform_output_to_ruby_version(stdout)
24
+ return app
25
+ end
26
+ end
27
+
28
+ raise NoRubyImplementationFoundError,
29
+ "No version of Ruby is found or provided"
30
+ end
31
+
32
+ def transform_output_to_ruby_version(output)
33
+ output.strip.split(" ")[0..1].join("-")
34
+ end
35
+
36
+ def fetch_ruby_implementation(ruby_implementation)
37
+ stdout, _stderr, status = Open3.capture3("#{ruby_implementation} -v")
38
+ [stdout, status]
39
+ end
40
+
41
+ RUBY_IMPLEMENTATIONS = %w[
42
+ ruby
43
+ truffleruby
44
+ jruby
45
+ mruby
46
+ rubinius
47
+ ].freeze
48
+
49
+ attr_accessor :app
50
+ end
51
+ end
52
+ end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "logger"
4
2
 
5
3
  module CreateRubyApp
@@ -7,8 +5,8 @@ module CreateRubyApp
7
5
  def initialize(
8
6
  name: "app",
9
7
  gems: [],
10
- version: RUBY_VERSION,
11
- logger: Logger.new(STDOUT)
8
+ version: nil,
9
+ logger: Logger.new($stdout)
12
10
  )
13
11
  @name = name
14
12
  @gems = gems
@@ -18,19 +16,22 @@ module CreateRubyApp
18
16
 
19
17
  def run!
20
18
  with_logger("Creating directories...", Actions::CreateDirectories)
19
+ with_logger(
20
+ "Setting Ruby implementations...",
21
+ Actions::SetRubyImplementation
22
+ )
21
23
  with_logger("Generating files...", Actions::GenerateFiles)
22
24
  with_logger("Making script executable...", Actions::MakeScriptExecutable)
23
25
  with_logger("Installing gems...", Actions::InstallGems)
24
- with_logger("Happy hacking!", Actions::NullAction)
26
+ with_logger("Happy hacking!", ->(_) {})
25
27
  end
26
28
 
27
29
  def classify_name
28
30
  name.split("_").collect(&:capitalize).join
29
31
  end
30
32
 
31
- attr_reader :name, :gems, :version, :logger
32
-
33
- RUBY_VERSION = "ruby-2.6.2"
33
+ attr_reader :name, :gems, :logger
34
+ attr_accessor :version
34
35
 
35
36
  private
36
37
 
@@ -1,52 +1,69 @@
1
- # frozen_string_literal: true
2
-
3
- require "thor"
1
+ require "dry/cli"
4
2
 
5
3
  module CreateRubyApp
6
- class CLI < Thor
7
- def self.exit_on_failure?
8
- true
9
- end
4
+ module Commands
5
+ extend Dry::CLI::Registry
6
+
7
+ class Version < Dry::CLI::Command
8
+ desc "Print the current version and exit"
10
9
 
11
- desc "version", "Print the current version and exit"
12
- map %w[--version -v] => :version
13
- def version
14
- say VERSION
10
+ def call(*)
11
+ puts VERSION
12
+ end
15
13
  end
16
14
 
17
- desc "new [NAME] [--ruby RUBY] [--gems GEMS]", "Generate new app"
18
- long_desc <<~DESC
19
- `create-ruby-app new NAME` will generate an app with the provided name.
20
-
21
- Examples:
22
- \x5$ create-ruby-app new my_app
23
- \x5$ create-ruby-app new -g sinatra,sequel -r ruby-2.6.0 web_app
24
- \x5$ create-ruby-app new --ruby jruby-2.9.6.0 my_app
25
- DESC
26
- method_option(
27
- :ruby,
28
- aliases: "-r",
29
- desc: "Specify which Ruby version to use for the project",
30
- default: App::RUBY_VERSION
31
- )
32
- method_option(
33
- :gems,
34
- aliases: "-g",
35
- desc: "Specify which gems to add to the project",
36
- default: ""
37
- )
38
- def new(name)
39
- App.new(
40
- name: replace_dashes_with_underscores(name),
41
- gems: options[:gems].split(","),
42
- version: options[:ruby]
43
- ).run!
15
+ class New < Dry::CLI::Command
16
+ desc "Generate new Ruby application"
17
+
18
+ argument :name, required: true, desc: "Application name"
19
+
20
+ option(
21
+ :ruby,
22
+ aliases: ["-r"],
23
+ desc: "Specify which Ruby version to use for the project"
24
+ )
25
+ option(
26
+ :gems,
27
+ aliases: ["-g"],
28
+ desc: "Specify which gems to add to the project",
29
+ default: ""
30
+ )
31
+
32
+ example [
33
+ "my_app",
34
+ "-r ruby-3.4.5 my_app",
35
+ "-g sinatra,sequel web_app",
36
+ "--ruby jruby-2.9.6.0 --gems rspec,pry api_service"
37
+ ]
38
+
39
+ def call(name:, **options)
40
+ App.new(
41
+ name: replace_dashes_with_underscores(name),
42
+ gems: parse_gems(options[:gems]),
43
+ version: options[:ruby]
44
+ ).run!
45
+ end
46
+
47
+ private
48
+
49
+ def replace_dashes_with_underscores(name)
50
+ name.tr("-", "_")
51
+ end
52
+
53
+ def parse_gems(gems_string)
54
+ return [] if gems_string.nil? || gems_string.strip.empty?
55
+
56
+ gems_string.split(",").map(&:strip).reject(&:empty?)
57
+ end
44
58
  end
45
59
 
46
- private
60
+ register "version", Version, aliases: ["--version", "-v"]
61
+ register "new", New
62
+ end
47
63
 
48
- def replace_dashes_with_underscores(name)
49
- name.tr("-", "_")
64
+ class CLI
65
+ def self.call
66
+ Dry::CLI.new(Commands).call
50
67
  end
51
68
  end
52
69
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  source "https://rubygems.org"
4
2
 
5
3
  group :test, :development do
@@ -7,6 +5,6 @@ group :test, :development do
7
5
  end
8
6
  <% unless locals[:gems].empty? %>
9
7
 
10
- <%= locals[:gems].map {|gem| "gem \"#{gem}\"" }.join("\n") %>
8
+ <%= locals[:gems] %>
11
9
 
12
10
  <% end %>
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module <%= locals[:app] %>
4
2
  class <%= locals[:app] %>
5
3
  end
@@ -1,3 +1 @@
1
1
  #!/usr/bin/env ruby
2
-
3
- # frozen_string_literal: true
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "rspec"
4
2
  require_relative "../lib/<%= locals[:app] %>"
5
3
 
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module CreateRubyApp
4
- VERSION = "1.1.0"
2
+ VERSION = "1.3.0"
5
3
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require_relative "create_ruby_app/app"
4
2
  require_relative "create_ruby_app/cli"
5
3
  require_relative "create_ruby_app/version"
@@ -7,7 +5,7 @@ require_relative "create_ruby_app/actions/create_directories"
7
5
  require_relative "create_ruby_app/actions/generate_files"
8
6
  require_relative "create_ruby_app/actions/install_gems"
9
7
  require_relative "create_ruby_app/actions/make_script_executable"
10
- require_relative "create_ruby_app/actions/null_action"
8
+ require_relative "create_ruby_app/actions/set_ruby_implementation"
11
9
 
12
10
  module CreateRubyApp
13
11
  class CreateRubyApp
@@ -0,0 +1,170 @@
1
+ require_relative "../spec_helper"
2
+ require "tmpdir"
3
+ require "fileutils"
4
+
5
+ RSpec.describe "App creation" do
6
+ let(:temp_dir) { Dir.mktmpdir }
7
+ let(:original_dir) { Dir.pwd }
8
+
9
+ around do |example|
10
+ Dir.chdir(original_dir)
11
+ Dir.chdir(temp_dir) do
12
+ example.run
13
+ end
14
+
15
+ FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir)
16
+ end
17
+
18
+ describe "creating an app with just a name" do
19
+ let(:app_name) { "test_app" }
20
+ let(:app) do
21
+ CreateRubyApp::App.new(
22
+ name: app_name,
23
+ gems: [],
24
+ version: nil,
25
+ logger: Logger.new(IO::NULL)
26
+ )
27
+ end
28
+
29
+ it "creates a complete app structure with the detected Ruby version" do
30
+ app.run!
31
+
32
+ # Check directory structure
33
+ expect(Dir.exist?(app_name)).to be(true)
34
+ expect(Dir.exist?("#{app_name}/bin")).to be(true)
35
+ expect(Dir.exist?("#{app_name}/lib")).to be(true)
36
+ expect(Dir.exist?("#{app_name}/spec")).to be(true)
37
+
38
+ # Check if main files exist
39
+ expect(File.exist?("#{app_name}/Gemfile")).to be(true)
40
+ expect(File.exist?("#{app_name}/.ruby-version")).to be(true)
41
+ expect(File.exist?("#{app_name}/lib/#{app_name}.rb")).to be(true)
42
+ expect(File.exist?("#{app_name}/bin/#{app_name}")).to be(true)
43
+ expect(File.exist?("#{app_name}/spec/spec_helper.rb")).to be(true)
44
+
45
+ # Check that a Ruby version was detected and set
46
+ expect(app.version).to match(
47
+ /^(ruby|jruby|truffleruby|mruby|rubinius)-\d+\.\d+/
48
+ )
49
+
50
+ # Check that the `.ruby-version` file contains the detected version
51
+ ruby_version_content = File.read("#{app_name}/.ruby-version")
52
+ expect(ruby_version_content.strip).to eq(app.version)
53
+
54
+ # Check if the main library file has the correct module and
55
+ # class structure
56
+ lib_content = File.read("#{app_name}/lib/#{app_name}.rb")
57
+ expect(lib_content).to include("module TestApp")
58
+ expect(lib_content).to include("class TestApp")
59
+
60
+ # Check if `spec_helper.rb` references the app correctly
61
+ spec_helper_content = File.read("#{app_name}/spec/spec_helper.rb")
62
+ expect(spec_helper_content).to include(
63
+ "require_relative \"../lib/#{app_name}\""
64
+ )
65
+
66
+ # Check that the executable script exists and is executable
67
+ expect(File.executable?("#{app_name}/bin/#{app_name}")).to be(true)
68
+
69
+ # Check that `Gemfile` has a basic structure
70
+ gemfile_content = File.read("#{app_name}/Gemfile")
71
+ expect(gemfile_content).to include('source "https://rubygems.org"')
72
+ end
73
+ end
74
+
75
+ describe "creating an app with a specified Ruby version" do
76
+ let(:app_name) { "custom_app" }
77
+ let(:specified_version) { "ruby-3.1.0" }
78
+ let(:app) do
79
+ CreateRubyApp::App.new(
80
+ name: app_name,
81
+ gems: %w[rspec pry],
82
+ version: specified_version,
83
+ logger: Logger.new(IO::NULL)
84
+ )
85
+ end
86
+
87
+ it "creates the app structure with the specified Ruby version and gems" do
88
+ app.run!
89
+
90
+ # Check directory structure
91
+ expect(Dir.exist?(app_name)).to be(true)
92
+ expect(Dir.exist?("#{app_name}/bin")).to be(true)
93
+ expect(Dir.exist?("#{app_name}/lib")).to be(true)
94
+ expect(Dir.exist?("#{app_name}/spec")).to be(true)
95
+
96
+ # Check that the version remains as specified
97
+ expect(app.version).to eq(specified_version)
98
+
99
+ # Check that the `.ruby-version` file contains the specified version
100
+ ruby_version_content = File.read("#{app_name}/.ruby-version")
101
+ expect(ruby_version_content.strip).to eq(specified_version)
102
+
103
+ # Check if `Gemfile` includes the specified gems
104
+ gemfile_content = File.read("#{app_name}/Gemfile")
105
+ expect(gemfile_content).to include('source "https://rubygems.org"')
106
+ expect(gemfile_content).to include('gem "pry"')
107
+ expect(gemfile_content).to include('gem "rspec"')
108
+
109
+ # Check if the main library file has the correct module and
110
+ # class structure
111
+ lib_content = File.read("#{app_name}/lib/#{app_name}.rb")
112
+ expect(lib_content).to include("module CustomApp")
113
+ expect(lib_content).to include("class CustomApp")
114
+ end
115
+ end
116
+
117
+ describe "creating an app with dashes in name" do
118
+ let(:app_name) { "my-cool-app" }
119
+ let(:normalized_name) { "my_cool_app" }
120
+ let(:app) do
121
+ CreateRubyApp::App.new(
122
+ name: normalized_name,
123
+ gems: [],
124
+ version: "ruby-3.2.0",
125
+ logger: Logger.new(IO::NULL)
126
+ )
127
+ end
128
+
129
+ it "normalizes the name correctly in files and classes" do
130
+ app.run!
131
+
132
+ # Check if the directory uses the normalized name
133
+ expect(Dir.exist?(normalized_name)).to be true
134
+
135
+ # Check if the main library file has the correct class name
136
+ lib_content = File.read("#{normalized_name}/lib/#{normalized_name}.rb")
137
+ expect(lib_content).to include("module MyCoolApp")
138
+ expect(lib_content).to include("class MyCoolApp")
139
+
140
+ # Check if `spec_helper.rb` references the correct file
141
+ spec_helper_content = File.read("#{normalized_name}/spec/spec_helper.rb")
142
+ expect(spec_helper_content).to include("require_relative \"../lib/#{normalized_name}\"")
143
+ end
144
+ end
145
+
146
+ describe "error handling" do
147
+ let(:app_name) { "error_test_app" }
148
+
149
+ it "raises an appropriate error when no Ruby implementation is found" do
150
+ app = CreateRubyApp::App.new(
151
+ name: app_name,
152
+ gems: [],
153
+ version: nil,
154
+ logger: Logger.new(IO::NULL)
155
+ )
156
+
157
+ # Mock all Ruby implementations to fail
158
+ %w[ruby truffleruby jruby mruby rubinius].each do |impl|
159
+ allow(Open3).to receive(:capture3).with("#{impl} -v").and_return(
160
+ ["", "", instance_double(Process::Status, success?: false)]
161
+ )
162
+ end
163
+
164
+ expect { app.run! }.to raise_error(
165
+ CreateRubyApp::Actions::NoRubyImplementationFoundError,
166
+ "No version of Ruby is found or provided"
167
+ )
168
+ end
169
+ end
170
+ end
@@ -1,10 +1,8 @@
1
- # frozen_string_literal: true
2
-
3
1
  require_relative "../../../spec_helper"
4
2
 
5
3
  RSpec.describe CreateRubyApp::Actions::CreateDirectories do
6
4
  describe ".call" do
7
- let(:app) { instance_double("app", name: "foo_bar") }
5
+ let(:app) { instance_double(CreateRubyApp::App, name: "foo_bar") }
8
6
  let(:action) { described_class }
9
7
 
10
8
  it "creates the directories" do
@@ -1,16 +1,12 @@
1
- # frozen_string_literal: true
2
-
3
1
  require_relative "../../../spec_helper"
4
2
 
5
3
  RSpec.describe CreateRubyApp::Actions::GenerateFiles do
6
4
  describe "#generate_gemfile" do
7
5
  context "when no additional gems are added" do
8
- let(:app) { instance_double("app", gems: []) }
6
+ let(:app) { instance_double(CreateRubyApp::App, gems: []) }
9
7
  let(:action) { described_class.new(app) }
10
8
  let(:gemfile) do
11
9
  <<~GEMFILE
12
- # frozen_string_literal: true
13
-
14
10
  source "https://rubygems.org"
15
11
 
16
12
  group :test, :development do
@@ -25,12 +21,12 @@ RSpec.describe CreateRubyApp::Actions::GenerateFiles do
25
21
  end
26
22
 
27
23
  context "when additional gems are added" do
28
- let(:app) { instance_double("app", gems: %w[sinatra sqlite]) }
24
+ let(:app) do
25
+ instance_double(CreateRubyApp::App, gems: %w[sinatra sqlite])
26
+ end
29
27
  let(:action) { described_class.new(app) }
30
28
  let(:gemfile) do
31
29
  <<~GEMFILE
32
- # frozen_string_literal: true
33
-
34
30
  source "https://rubygems.org"
35
31
 
36
32
  group :test, :development do
@@ -51,15 +47,18 @@ RSpec.describe CreateRubyApp::Actions::GenerateFiles do
51
47
  describe "#generate_ruby_version_file" do
52
48
  context "when no version is specified" do
53
49
  let(:app) do
54
- instance_double("app", version: CreateRubyApp::App::RUBY_VERSION)
50
+ instance_double(
51
+ CreateRubyApp::App,
52
+ version: "ruby-2.7.1"
53
+ )
55
54
  end
56
55
  let(:action) { described_class.new(app) }
57
56
 
58
- it { expect(action.ruby_version_file).to eq("ruby-2.6.2") }
57
+ it { expect(action.ruby_version_file).to eq("ruby-2.7.1") }
59
58
  end
60
59
 
61
60
  context "when a version is specified" do
62
- let(:app) { instance_double("app", version: "ruby-2.5.0") }
61
+ let(:app) { instance_double(CreateRubyApp::App, version: "ruby-2.5.0") }
63
62
  let(:action) { described_class.new(app) }
64
63
 
65
64
  it { expect(action.ruby_version_file).to eq("ruby-2.5.0") }
@@ -68,12 +67,10 @@ RSpec.describe CreateRubyApp::Actions::GenerateFiles do
68
67
 
69
68
  describe "#generate_lib_file" do
70
69
  context "when generating a lib file" do
71
- let(:app) { instance_double("app") }
70
+ let(:app) { instance_double(CreateRubyApp::App) }
72
71
  let(:action) { described_class.new(app) }
73
72
  let(:lib_file) do
74
73
  <<~LIB_FILE
75
- # frozen_string_literal: true
76
-
77
74
  module ThisIsAnApp
78
75
  class ThisIsAnApp
79
76
  end
@@ -91,12 +88,10 @@ RSpec.describe CreateRubyApp::Actions::GenerateFiles do
91
88
 
92
89
  describe "#generate_spec_helper_file" do
93
90
  context "when generating a spec helper file" do
94
- let(:app) { instance_double("app", name: "this_is_an_app") }
91
+ let(:app) { instance_double(CreateRubyApp::App, name: "this_is_an_app") }
95
92
  let(:action) { described_class.new(app) }
96
93
  let(:spec_helper) do
97
94
  <<~SPEC_HELPER
98
- # frozen_string_literal: true
99
-
100
95
  require "rspec"
101
96
  require_relative "../lib/this_is_an_app"
102
97
 
@@ -118,13 +113,11 @@ RSpec.describe CreateRubyApp::Actions::GenerateFiles do
118
113
 
119
114
  describe "#generate_script_file" do
120
115
  context "when generating an executable script file" do
121
- let(:app) { instance_double("app") }
116
+ let(:app) { instance_double(CreateRubyApp::App) }
122
117
  let(:action) { described_class.new(app) }
123
118
  let(:script_file) do
124
119
  <<~SCRIPT_FILE
125
120
  #!/usr/bin/env ruby
126
-
127
- # frozen_string_literal: true
128
121
  SCRIPT_FILE
129
122
  end
130
123
 
@@ -1,10 +1,8 @@
1
- # frozen_string_literal: true
2
-
3
1
  require_relative "../../../spec_helper"
4
2
 
5
3
  RSpec.describe CreateRubyApp::Actions::InstallGems do
6
4
  describe ".call" do
7
- let(:app) { instance_double("app", name: "foo_bar") }
5
+ let(:app) { instance_double(CreateRubyApp::App, name: "foo_bar") }
8
6
  let(:action) { described_class }
9
7
 
10
8
  it "installs the gems" do
@@ -1,10 +1,8 @@
1
- # frozen_string_literal: true
2
-
3
1
  require_relative "../../../spec_helper"
4
2
 
5
3
  RSpec.describe CreateRubyApp::Actions::MakeScriptExecutable do
6
4
  describe ".call" do
7
- let(:app) { instance_double("app", name: "foo_bar") }
5
+ let(:app) { instance_double(CreateRubyApp::App, name: "foo_bar") }
8
6
  let(:action) { described_class }
9
7
 
10
8
  it "creates the directories" do