hanami-cli 2.2.1 → 2.3.0.beta1

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -0
  3. data/CHANGELOG.md +33 -0
  4. data/Gemfile +3 -1
  5. data/hanami-cli.gemspec +2 -0
  6. data/lib/hanami/cli/command.rb +5 -11
  7. data/lib/hanami/cli/commands/app/command.rb +5 -5
  8. data/lib/hanami/cli/commands/app/console.rb +1 -0
  9. data/lib/hanami/cli/commands/app/db/command.rb +0 -1
  10. data/lib/hanami/cli/commands/app/db/drop.rb +2 -2
  11. data/lib/hanami/cli/commands/app/db/rollback.rb +204 -0
  12. data/lib/hanami/cli/commands/app/db/utils/mysql.rb +3 -2
  13. data/lib/hanami/cli/commands/app/db/utils/postgres.rb +3 -1
  14. data/lib/hanami/cli/commands/app/generate/action.rb +32 -40
  15. data/lib/hanami/cli/commands/app/generate/command.rb +54 -17
  16. data/lib/hanami/cli/commands/app/generate/component.rb +4 -19
  17. data/lib/hanami/cli/commands/app/generate/part.rb +4 -21
  18. data/lib/hanami/cli/commands/app/generate/slice.rb +2 -2
  19. data/lib/hanami/cli/commands/app/generate/view.rb +5 -24
  20. data/lib/hanami/cli/commands/app/server.rb +0 -1
  21. data/lib/hanami/cli/commands/app.rb +1 -0
  22. data/lib/hanami/cli/commands/gem/new.rb +34 -3
  23. data/lib/hanami/cli/errors.rb +26 -0
  24. data/lib/hanami/cli/files.rb +20 -6
  25. data/lib/hanami/cli/generators/app/action.rb +78 -101
  26. data/lib/hanami/cli/generators/app/component.rb +11 -33
  27. data/lib/hanami/cli/generators/app/migration.rb +1 -1
  28. data/lib/hanami/cli/generators/app/operation.rb +4 -5
  29. data/lib/hanami/cli/generators/app/part.rb +42 -65
  30. data/lib/hanami/cli/generators/app/relation.rb +4 -5
  31. data/lib/hanami/cli/generators/app/repo.rb +3 -5
  32. data/lib/hanami/cli/generators/app/ruby_class_file.rb +32 -0
  33. data/lib/hanami/cli/generators/app/ruby_file.rb +128 -0
  34. data/lib/hanami/cli/generators/app/ruby_module_file.rb +28 -0
  35. data/lib/hanami/cli/generators/app/slice.rb +130 -37
  36. data/lib/hanami/cli/generators/app/struct.rb +3 -4
  37. data/lib/hanami/cli/generators/app/view.rb +40 -45
  38. data/lib/hanami/cli/generators/context.rb +6 -0
  39. data/lib/hanami/cli/generators/gem/app/assets.js +14 -13
  40. data/lib/hanami/cli/generators/gem/app/dev +1 -1
  41. data/lib/hanami/cli/generators/gem/app/gemfile.erb +5 -0
  42. data/lib/hanami/cli/generators/gem/app/gitignore.erb +3 -1
  43. data/lib/hanami/cli/generators/gem/app/rakefile.erb +3 -0
  44. data/lib/hanami/cli/generators/gem/app/readme.erb +14 -0
  45. data/lib/hanami/cli/generators/gem/app.rb +40 -37
  46. data/lib/hanami/cli/ruby_file_generator.rb +17 -8
  47. data/lib/hanami/cli/server.rb +15 -1
  48. data/lib/hanami/cli/version.rb +1 -1
  49. data/lib/hanami/console/context.rb +5 -0
  50. metadata +35 -41
  51. data/lib/hanami/cli/generators/app/action/action.erb +0 -17
  52. data/lib/hanami/cli/generators/app/action/slice_action.erb +0 -17
  53. data/lib/hanami/cli/generators/app/action/slice_template.html.erb +0 -1
  54. data/lib/hanami/cli/generators/app/action/slice_view.erb +0 -10
  55. data/lib/hanami/cli/generators/app/action/template.erb +0 -0
  56. data/lib/hanami/cli/generators/app/action/template.html.erb +0 -1
  57. data/lib/hanami/cli/generators/app/action/view.erb +0 -10
  58. data/lib/hanami/cli/generators/app/action_context.rb +0 -90
  59. data/lib/hanami/cli/generators/app/component/component.erb +0 -8
  60. data/lib/hanami/cli/generators/app/component/slice_component.erb +0 -8
  61. data/lib/hanami/cli/generators/app/component_context.rb +0 -82
  62. data/lib/hanami/cli/generators/app/part/app_base_part.erb +0 -9
  63. data/lib/hanami/cli/generators/app/part/app_part.erb +0 -13
  64. data/lib/hanami/cli/generators/app/part/slice_base_part.erb +0 -9
  65. data/lib/hanami/cli/generators/app/part/slice_part.erb +0 -13
  66. data/lib/hanami/cli/generators/app/part_context.rb +0 -82
  67. data/lib/hanami/cli/generators/app/ruby_file_writer.rb +0 -151
  68. data/lib/hanami/cli/generators/app/slice/action.erb +0 -7
  69. data/lib/hanami/cli/generators/app/slice/app_css.erb +0 -5
  70. data/lib/hanami/cli/generators/app/slice/app_js.erb +0 -1
  71. data/lib/hanami/cli/generators/app/slice/app_layout.erb +0 -18
  72. data/lib/hanami/cli/generators/app/slice/helpers.erb +0 -10
  73. data/lib/hanami/cli/generators/app/slice/keep.erb +0 -0
  74. data/lib/hanami/cli/generators/app/slice/operation.erb +0 -7
  75. data/lib/hanami/cli/generators/app/slice/relation.erb +0 -8
  76. data/lib/hanami/cli/generators/app/slice/repo.erb +0 -8
  77. data/lib/hanami/cli/generators/app/slice/routes.erb +0 -3
  78. data/lib/hanami/cli/generators/app/slice/struct.erb +0 -8
  79. data/lib/hanami/cli/generators/app/slice/view.erb +0 -7
  80. data/lib/hanami/cli/generators/app/slice_context.rb +0 -72
  81. data/lib/hanami/cli/generators/app/view/app_template.html.erb +0 -1
  82. data/lib/hanami/cli/generators/app/view/app_view.erb +0 -10
  83. data/lib/hanami/cli/generators/app/view/slice_template.html.erb +0 -1
  84. data/lib/hanami/cli/generators/app/view/slice_view.erb +0 -10
  85. data/lib/hanami/cli/generators/app/view_context.rb +0 -88
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f7c2e2213ec21d8e65e4a07b1f5035d082e58ff8e5f0368112aa58b4be53a4f2
4
- data.tar.gz: 7191dcdef8dd7683ada1caf68fab01c6047c3b9368006ce821d01453b77fb00d
3
+ metadata.gz: c770a27b290fd7bb7fcdd8db847ebd27723088374f8799b59d6e707e59404da0
4
+ data.tar.gz: 35aeda59b2833925c79d5ca3f604c410bb5512ec608c9f9527d7d8cbd1818c47
5
5
  SHA512:
6
- metadata.gz: 7ee16c77ac4c7f721c02678ba7bbe1735ba84f66b41a04c738706ab704600230b58ae573c12a3b460851bffb7177403c7df402f8242afdd35949a670b6c30595
7
- data.tar.gz: a93f0f48ae108ecdcd0f33812b0c83c09a88e9891c3712edd461c55cba09b3b96df3067828ff3f14cad0bc03e320cb634fc9098fd6777107f4d75b8dfec88c57
6
+ metadata.gz: e8a06cf2ca8224b1216c62d62356af0dc3ab4620b762fef531f0dec5b2c792816afe7f5a9a828170621a94f58370f04047ce0c86922e2730465417df9793169c
7
+ data.tar.gz: fb87833f3c11436dd3f313d9a3900d89395f6a8edef49a14a5e7b5c981081ec0bf859c11892d43502a005afd427bff7e8331faa733d1ec016875cef7fe59ad18
@@ -24,9 +24,14 @@ jobs:
24
24
  fail-fast: false
25
25
  matrix:
26
26
  ruby:
27
+ - "3.4"
27
28
  - "3.3"
28
29
  - "3.2"
29
30
  - "3.1"
31
+ rack:
32
+ - "~> 2.0"
33
+ - "~> 3.0"
34
+ name: tests (Ruby ${{ matrix.ruby }}, Rack ${{ matrix.rack }})
30
35
  env:
31
36
  POSTGRES_BASE_URL: postgres://postgres:password@localhost:5432/hanami_cli_test
32
37
  steps:
@@ -38,8 +43,12 @@ jobs:
38
43
  with:
39
44
  ruby-version: ${{matrix.ruby}}
40
45
  bundler-cache: true
46
+ env:
47
+ RACK_VERSION_CONSTRAINT: ${{matrix.rack}}
41
48
  - name: Run all tests
42
49
  run: bundle exec rake spec
50
+ env:
51
+ RACK_VERSION_CONSTRAINT: ${{matrix.rack}}
43
52
  services:
44
53
  mysql:
45
54
  image: mysql:latest
data/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  Hanami Command Line Interface
4
4
 
5
+ ## Unreleased
6
+
7
+ ## v2.3.0.beta1 - 2025-10-03
8
+
9
+ ### Added
10
+
11
+ - Running `hanami generate` commands within a slice directory will generate the file in that slice (@krzykamil in #298)
12
+ - Add `db rollback` command, supporting rolling back a single database at a time. (@krzykamil in #300)
13
+ - `console` command loads configured extensions from app config. Add these using e.g. `config.console.include MyModule, AnotherModule` (@alassek in #324)
14
+ - `console` command uses REPL engine configured in app config. Set this using e.g. `config.console.engine = :pry`; valid options are `:irb` (default) and `:pry` (@alassek in #324)
15
+
16
+ ### Fixed
17
+
18
+ - Allow generated `public/400.html` and `public/500.html` to go into source control (@kyleplump in #290)
19
+ - Properly show database errors from failed `db drop` commands (@katafrakt in #281)
20
+ - Ensure consistent env var loading by disallowing Foreman's own env processing in generated `bin/dev` script (@cflipse in #305)
21
+ - Use the configured app inflector (and any custom inflections) for all commands (@timriley in #312)
22
+ - For app generated with `hanami new` with `--head`, include `hanami-cli` in the `Gemfile` (@afomera in #328)
23
+
24
+ ### Changed
25
+
26
+ - Prevent `hanami new` from creating apps with confusing names (currently: "app" or "slice") (@seven1m in #272)
27
+ - Provide more helpful instructions in generated app README (@hanarimawi in #273)
28
+ - Prevent generators from overwriting files (@maxemitchell in #274, @stephannv in #319)
29
+ - Add irb as a gem dependency to avoid default gem warnings (@y-yagi in #294)
30
+ - Expand on comments `config/assets.js` and enable customization function by default (@robyurkowski in #293)
31
+ - Run `git init` at the end of `hanami new` (@krzykamil in #295)
32
+ - Prevent blank lines from showing when generating classes without deep module nesting (@cllns)
33
+ - Add `--skip-view` flag to `hanami new` (@kyleplump in #308)
34
+ - Support Rack 3 in addition to Rack 2 (for `hanami server` command) (@kyleplump in #289)
35
+ - Ensure compatibility with MySQL 9.4's CLI tools in `db structure load` command (@timriley in #315)
36
+ - Generated `Rakefile` will load tasks defined in `lib/tasks/` (@AlexanderZagaynov in #318)
37
+
5
38
  ## v2.2.1 - 2024-11-12
6
39
 
7
40
  ### Changed
data/Gemfile CHANGED
@@ -18,7 +18,9 @@ gem "hanami-utils", github: "hanami/utils", branch: "main"
18
18
 
19
19
  gem "dry-system", github: "dry-rb/dry-system", branch: "main"
20
20
 
21
- gem "rack"
21
+ if ENV["RACK_VERSION_CONSTRAINT"]
22
+ gem "rack", ENV["RACK_VERSION_CONSTRAINT"]
23
+ end
22
24
 
23
25
  gem "mysql2"
24
26
  gem "pg"
data/hanami-cli.gemspec CHANGED
@@ -34,8 +34,10 @@ Gem::Specification.new do |spec|
34
34
  spec.add_dependency "dry-cli", "~> 1.0", ">= 1.1.0"
35
35
  spec.add_dependency "dry-files", "~> 1.0", ">= 1.0.2", "< 2"
36
36
  spec.add_dependency "dry-inflector", "~> 1.0", "< 2"
37
+ spec.add_dependency "irb"
37
38
  spec.add_dependency "rake", "~> 13.0"
38
39
  spec.add_dependency "zeitwerk", "~> 2.6"
40
+ spec.add_dependency "rackup"
39
41
 
40
42
  spec.add_development_dependency "rspec", "~> 3.9"
41
43
  spec.add_development_dependency "rubocop", "~> 1.0"
@@ -24,7 +24,6 @@ module Hanami
24
24
  out: $stdout,
25
25
  err: $stderr,
26
26
  fs: Hanami::CLI::Files.new(out: out),
27
- inflector: Dry::Inflector.new,
28
27
  **opts
29
28
  )
30
29
  super
@@ -46,12 +45,15 @@ module Hanami
46
45
  #
47
46
  # @since 2.0.0
48
47
  # @api public
49
- def initialize(out:, err:, fs:, inflector:)
48
+ def initialize(out:, err:, fs:)
50
49
  super()
51
50
  @out = out
52
51
  @err = err
53
52
  @fs = fs
54
- @inflector = inflector
53
+ end
54
+
55
+ def inflector
56
+ @inflector ||= Dry::Inflector.new
55
57
  end
56
58
 
57
59
  private
@@ -79,14 +81,6 @@ module Hanami
79
81
  # @since 2.0.0
80
82
  # @api public
81
83
  attr_reader :fs
82
-
83
- # Returns the inflector.
84
- #
85
- # @return [Dry::Inflector]
86
- #
87
- # @since 2.0.0
88
- # @api public
89
- attr_reader :inflector
90
84
  end
91
85
  end
92
86
  end
@@ -14,10 +14,6 @@ module Hanami
14
14
  # @since 2.0.0
15
15
  # @api public
16
16
  class Command < Hanami::CLI::Command
17
- # @since 2.0.0
18
- # @api private
19
- ACTION_SEPARATOR = "." # TODO: rename to container key separator
20
-
21
17
  # Overloads {Hanami::CLI::Commands::App::Command#call} to ensure an appropriate
22
18
  # `HANAMI_ENV` environment variable is set.
23
19
  #
@@ -44,6 +40,9 @@ module Hanami
44
40
  Hanami::Env.load
45
41
 
46
42
  super
43
+ rescue FileAlreadyExistsError => error
44
+ err.puts(error.message)
45
+ exit(1)
47
46
  end
48
47
  end
49
48
 
@@ -70,6 +69,8 @@ module Hanami
70
69
  end
71
70
  end
72
71
 
72
+ def inflector = app.inflector
73
+
73
74
  # Runs another CLI command via its command class.
74
75
  #
75
76
  # @param klass [Hanami::CLI::Command]
@@ -80,7 +81,6 @@ module Hanami
80
81
  def run_command(klass, ...)
81
82
  klass.new(
82
83
  out: out,
83
- inflector: app.inflector,
84
84
  fs: Hanami::CLI::Files,
85
85
  ).call(...)
86
86
  end
@@ -38,6 +38,7 @@ module Hanami
38
38
  # @since 2.0.0
39
39
  # @api private
40
40
  def call(engine: nil, **opts)
41
+ engine ||= app.config.console.engine.to_s
41
42
  console_engine = resolve_engine(engine, opts)
42
43
 
43
44
  if console_engine.nil?
@@ -39,7 +39,6 @@ module Hanami
39
39
  def run_command(klass, ...)
40
40
  klass.new(
41
41
  out: out,
42
- inflector: inflector,
43
42
  fs: fs,
44
43
  system_call: system_call,
45
44
  test_env_executor: test_env_executor,
@@ -11,7 +11,7 @@ module Hanami
11
11
 
12
12
  option :gateway, required: false, desc: "Use database for gateway"
13
13
 
14
- def call(app: false, slice: nil, gateway: nil, **)
14
+ def call(app: false, slice: nil, gateway: nil, command_exit: method(:exit), **)
15
15
  exit_codes = []
16
16
 
17
17
  databases(app: app, slice: slice, gateway: gateway).each do |database|
@@ -27,7 +27,7 @@ module Hanami
27
27
  end
28
28
 
29
29
  exit_codes.each do |code|
30
- break exit code if code > 0
30
+ break command_exit.(code) if code > 0
31
31
  end
32
32
 
33
33
  re_run_development_command_in_test
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../../app/command"
3
+ require_relative "structure/dump"
4
+
5
+ module Hanami
6
+ module CLI
7
+ module Commands
8
+ module App
9
+ module DB
10
+ class Rollback < DB::Command
11
+ desc "Rollback database to a previous migration"
12
+
13
+ argument :steps, desc: "Number of migrations to rollback", required: false
14
+ option :target, desc: "Target migration number", aliases: ["-t"]
15
+ option :dump, desc: "Dump structure after rolling back", default: true
16
+ option :gateway, required: false, desc: "Use database for gateway"
17
+
18
+ def call(steps: nil, app: false, slice: nil, gateway: nil, target: nil, dump: true, command_exit: method(:exit), **)
19
+ # We allow either a number of steps or a target migration number to be provided
20
+ # If steps is provided and target is not, we use steps as the target migration number, but we also have to
21
+ # make sure steps is a number, hence some additional logic around checking and converting to number
22
+ target = steps if steps && !target && !code_is_number?(steps)
23
+ steps_count = steps && code_is_number?(steps) ? Integer(steps) : 1
24
+
25
+ database = resolve_target_database(app: app, slice: slice, gateway: gateway, command_exit: command_exit)
26
+ return unless database
27
+
28
+ migration_code, migration_name = find_migration_target(target, steps_count, database)
29
+
30
+ if migration_name.nil?
31
+ output = if steps && code_is_number?(steps)
32
+ "==> migration file for #{steps} steps back was not found"
33
+ elsif target
34
+ "==> migration file for target #{target} was not found"
35
+ else
36
+ "==> no migrations to rollback"
37
+ end
38
+
39
+ out.puts output
40
+ return
41
+ end
42
+
43
+ measure "database #{database.name} rolled back to #{migration_name}" do
44
+ database.run_migrations(target: Integer(migration_code))
45
+ true
46
+ end
47
+
48
+ return unless dump && !re_running_in_test?
49
+
50
+ run_command(
51
+ Structure::Dump,
52
+ app: database.slice == self.app,
53
+ slice: database.slice == self.app ? nil : database.slice.slice_name.to_s,
54
+ gateway: database.gateway_name == :default ? nil : database.gateway_name.to_s,
55
+ command_exit: command_exit
56
+ )
57
+ end
58
+
59
+ private
60
+
61
+ def resolve_target_database(app:, slice:, gateway:, command_exit:)
62
+ if gateway && !app && !slice
63
+ err.puts "When specifying --gateway, an --app or --slice must also be given"
64
+ command_exit.(1)
65
+ return
66
+ end
67
+
68
+ if slice
69
+ resolve_slice_database(slice, gateway, command_exit)
70
+ elsif app
71
+ resolve_app_database(gateway, command_exit)
72
+ else
73
+ resolve_default_database(command_exit)
74
+ end
75
+ end
76
+
77
+ def resolve_slice_database(slice_name, gateway, command_exit)
78
+ slice = resolve_slice(slice_name, command_exit)
79
+ return unless slice
80
+
81
+ databases = build_databases(slice)
82
+
83
+ if gateway
84
+ database = databases[gateway.to_sym]
85
+ unless database
86
+ err.puts %(No gateway "#{gateway}" found in slice "#{slice_name}")
87
+ command_exit.(1)
88
+ return
89
+ end
90
+ database
91
+ elsif databases.size == 1
92
+ databases.values.first
93
+ else
94
+ err.puts "Multiple gateways found in slice #{slice_name}. Please specify --gateway option."
95
+ command_exit.(1)
96
+ end
97
+ end
98
+
99
+ def resolve_app_database(gateway, command_exit)
100
+ databases = build_databases(app)
101
+
102
+ if gateway
103
+ database = databases[gateway.to_sym]
104
+ unless database
105
+ err.puts %(No gateway "#{gateway}" found in app)
106
+ command_exit.(1)
107
+ return
108
+ end
109
+ database
110
+ elsif databases.size == 1
111
+ databases.values.first
112
+ else
113
+ err.puts "Multiple gateways found in app. Please specify --gateway option."
114
+ command_exit.(1)
115
+ end
116
+ end
117
+
118
+ def resolve_default_database(command_exit)
119
+ all_dbs = all_databases
120
+
121
+ if all_dbs.empty?
122
+ err.puts "No databases found"
123
+ command_exit.(1)
124
+ elsif all_dbs.size == 1
125
+ all_dbs.first
126
+ else
127
+ app_databases = build_databases(app)
128
+ if app_databases.size == 1
129
+ app_databases.values.first
130
+ elsif app_databases.size > 1
131
+ err.puts "Multiple gateways found in app. Please specify --gateway option."
132
+ command_exit.(1)
133
+ return
134
+ else
135
+ err.puts "Multiple database contexts found. Please specify --app or --slice option."
136
+ command_exit.(1)
137
+ return
138
+ end
139
+ end
140
+ end
141
+
142
+ def resolve_slice(slice_name, command_exit)
143
+ slice_name_sym = inflector.underscore(Shellwords.shellescape(slice_name)).to_sym
144
+ slice = app.slices[slice_name_sym]
145
+
146
+ unless slice
147
+ err.puts %(Slice "#{slice_name}" not found)
148
+ command_exit.(1)
149
+ return
150
+ end
151
+
152
+ ensure_database_slice(slice)
153
+ slice
154
+ end
155
+
156
+ def find_migration_target(target, steps_count, database)
157
+ applied_migrations = database.applied_migrations
158
+
159
+ return if applied_migrations.empty?
160
+
161
+ if applied_migrations.one? && target.nil?
162
+ return initial_state(applied_migrations)
163
+ end
164
+
165
+ if target
166
+ migration = applied_migrations.detect { |m| m.split("_").first == target }
167
+ migration_code = migration&.split("_")&.first
168
+ migration_name = migration ? File.basename(migration, ".*") : nil
169
+ else
170
+ # When rolling back N steps, we want to target the migration that is N steps back
171
+ # If we have migrations [A, B, C, D] and want to rollback 2 steps from D,
172
+ # we want to target B (index -3, since we go back 2 steps + 1 for the target)
173
+ target_index = -(steps_count + 1)
174
+
175
+ if target_index.abs > applied_migrations.size
176
+ return initial_state(applied_migrations)
177
+ else
178
+ migration = applied_migrations[target_index]
179
+ migration_code = migration&.split("_")&.first
180
+ migration_name = migration ? File.basename(migration, ".*") : nil
181
+ end
182
+ end
183
+
184
+ [migration_code, migration_name]
185
+ end
186
+
187
+ def initial_state(applied_migrations)
188
+ migration = applied_migrations.first
189
+
190
+ migration_code = Integer(migration.split("_").first) - 1
191
+ migration_name = "initial state"
192
+
193
+ [migration_code, migration_name]
194
+ end
195
+
196
+ def code_is_number?(code)
197
+ code&.to_s&.match?(/^\d+$/) && !code.to_s.match?(/^\d{10,}$/)
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
@@ -29,8 +29,9 @@ module Hanami
29
29
  # @since 2.2.0
30
30
  def exists?
31
31
  result = exec_cli("mysql", %(-e "SHOW DATABASES LIKE '#{name}'" --batch))
32
+ raise Hanami::CLI::DatabaseExistenceCheckError.new(result.err) unless result.successful?
32
33
 
33
- result.successful? && result.out != ""
34
+ result.out != ""
34
35
  end
35
36
 
36
37
  # @api private
@@ -47,7 +48,7 @@ module Hanami
47
48
  def exec_load_command
48
49
  exec_cli(
49
50
  "mysql",
50
- %(--execute "SET FOREIGN_KEY_CHECKS = 0; SOURCE #{structure_file}; SET FOREIGN_KEY_CHECKS = 1" --database #{escaped_name})
51
+ %(--commands --execute "SET FOREIGN_KEY_CHECKS = 0; SOURCE #{structure_file}; SET FOREIGN_KEY_CHECKS = 1" --database #{escaped_name})
51
52
  )
52
53
  end
53
54
 
@@ -33,7 +33,9 @@ module Hanami
33
33
  # @since 2.2.0
34
34
  def exists?
35
35
  result = system_call.call("psql -t -A -c '\\list #{escaped_name}'", env: cli_env_vars)
36
- result.successful? && result.out.include?("#{name}|") # start_with?
36
+ raise Hanami::CLI::DatabaseExistenceCheckError.new(result.err) unless result.successful?
37
+
38
+ result.out.include?("#{name}|") # start_with?
37
39
  end
38
40
 
39
41
  # @api private
@@ -13,7 +13,7 @@ module Hanami
13
13
  module Generate
14
14
  # @since 2.0.0
15
15
  # @api private
16
- class Action < App::Command
16
+ class Action < Command
17
17
  # TODO: ideally the default format should lookup
18
18
  # slice configuration (Action's `default_response_format`)
19
19
  DEFAULT_FORMAT = "html"
@@ -29,29 +29,37 @@ module Hanami
29
29
  private_constant :DEFAULT_SKIP_ROUTE
30
30
 
31
31
  argument :name, required: true, desc: "Action name"
32
- option :url, required: false, type: :string, desc: "Action URL"
33
- option :http, required: false, type: :string, desc: "Action HTTP method"
34
- # option :format, required: false, type: :string, default: DEFAULT_FORMAT, desc: "Template format"
32
+
33
+ option :url, as: :url_path, required: false, type: :string, desc: "Action URL path"
34
+
35
+ option :http, as: :http_method, required: false, type: :string, desc: "Action HTTP method"
36
+
35
37
  option \
36
38
  :skip_view,
37
39
  required: false,
38
40
  type: :flag,
39
41
  default: DEFAULT_SKIP_VIEW,
40
42
  desc: "Skip view and template generation"
43
+
44
+ # TODO: Implement this
41
45
  option \
42
46
  :skip_tests,
43
47
  required: false,
44
48
  type: :flag,
45
49
  default: DEFAULT_SKIP_TESTS,
46
50
  desc: "Skip test generation"
51
+
47
52
  option \
48
53
  :skip_route,
49
54
  required: false,
50
55
  type: :flag,
51
56
  default: DEFAULT_SKIP_ROUTE,
52
57
  desc: "Skip route generation"
58
+
53
59
  option :slice, required: false, desc: "Slice name"
54
60
 
61
+ # option :format, required: false, type: :string, default: DEFAULT_FORMAT, desc: "Template format"
62
+
55
63
  # rubocop:disable Layout/LineLength
56
64
  example [
57
65
  %(books.index # GET /books to: "books.index" (MyApp::Actions::Books::Index)),
@@ -68,52 +76,36 @@ module Hanami
68
76
  ]
69
77
  # rubocop:enable Layout/LineLength
70
78
 
71
- # @since 2.0.0
72
- # @api private
73
- def initialize(
74
- fs:, inflector:,
75
- naming: Naming.new(inflector: inflector),
76
- generator: Generators::App::Action.new(fs: fs, inflector: inflector),
77
- **opts
78
- )
79
- super(fs: fs, inflector: inflector, **opts)
80
-
81
- @naming = naming
82
- @generator = generator
79
+ def generator_class
80
+ Generators::App::Action
83
81
  end
84
82
 
85
- # rubocop:disable Metrics/ParameterLists
86
-
87
83
  # @since 2.0.0
88
84
  # @api private
85
+ # rubocop:disable Lint/ParameterLists
89
86
  def call(
90
87
  name:,
91
- url: nil,
92
- http: nil,
93
- format: DEFAULT_FORMAT,
88
+ slice: nil,
89
+ url_path: nil,
90
+ http_method: nil,
94
91
  skip_view: DEFAULT_SKIP_VIEW,
95
- skip_tests: DEFAULT_SKIP_TESTS, # rubocop:disable Lint/UnusedMethodArgument,
96
92
  skip_route: DEFAULT_SKIP_ROUTE,
97
- slice: nil,
98
- context: nil,
99
- **
93
+ skip_tests: DEFAULT_SKIP_TESTS # rubocop:disable Lint/UnusedMethodArgument
100
94
  )
101
- slice = inflector.underscore(Shellwords.shellescape(slice)) if slice
102
- name = naming.action_name(name)
103
- *controller, action = name.split(ACTION_SEPARATOR)
104
-
105
- if controller.empty?
106
- raise InvalidActionNameError.new(name)
107
- end
108
-
109
- generator.call(app.namespace, controller, action, url, http, format, skip_view, skip_route, slice, context: context)
95
+ name = Naming.new(inflector:).action_name(name)
96
+
97
+ raise InvalidActionNameError.new(name) unless name.include?(".")
98
+
99
+ super(
100
+ name:,
101
+ slice:,
102
+ url_path:,
103
+ skip_route:,
104
+ http_method:,
105
+ skip_view: skip_view || !Hanami.bundled?("hanami-view"),
106
+ )
110
107
  end
111
-
112
- # rubocop:enable Metrics/ParameterLists
113
-
114
- private
115
-
116
- attr_reader :naming, :generator
108
+ # rubocop:enable Lint/ParameterLists
117
109
  end
118
110
  end
119
111
  end
@@ -21,36 +21,73 @@ module Hanami
21
21
 
22
22
  # @since 2.2.0
23
23
  # @api private
24
- def initialize(
25
- fs:,
26
- inflector:,
27
- **opts
28
- )
24
+ def initialize(fs:, out:, **)
29
25
  super
30
- @generator = generator_class.new(fs: fs, inflector: inflector, out: out)
26
+ @generator = generator_class.new(fs:, inflector:, out:)
31
27
  end
32
28
 
29
+ # @since 2.2.0
30
+ # @api private
33
31
  def generator_class
34
- # Must be implemented by subclasses, with class that takes:
35
- # fs:, inflector:, out:
32
+ # Must be implemented by subclasses, with initialize method that takes:
33
+ # fs:, out:
36
34
  end
37
35
 
38
36
  # @since 2.2.0
39
37
  # @api private
40
- def call(name:, slice: nil, **)
41
- if slice
42
- generator.call(
43
- key: name,
44
- namespace: slice,
45
- base_path: fs.join("slices", inflector.underscore(slice))
46
- )
47
- else
38
+ def call(name:, slice: nil, **opts)
39
+ slice ||= detect_slice_from_cwd
40
+
41
+ if slice.nil?
48
42
  generator.call(
49
43
  key: name,
50
44
  namespace: app.namespace,
51
- base_path: "app"
45
+ base_path: "app",
46
+ **opts,
52
47
  )
48
+ return
53
49
  end
50
+
51
+ slice_root = slice.respond_to?(:root) ? slice.root : detect_slice_root(slice)
52
+ raise MissingSliceError.new(slice) unless fs.exist?(slice_root)
53
+
54
+ generator.call(
55
+ key: name,
56
+ namespace: slice,
57
+ base_path: slice_root,
58
+ **opts,
59
+ )
60
+ end
61
+
62
+ private
63
+
64
+ def detect_slice_from_cwd
65
+ slices_by_root = app.slices.with_nested.each.to_h { |slice| [slice.root.to_s, slice] }
66
+ slices_by_root[fs.pwd]
67
+ end
68
+
69
+ # Returns the root for the given slice name.
70
+ #
71
+ # This currently works with top-level slices only, and it simply appends the slice's
72
+ # name onto the "slices/" dir, returning e.g. "slices/main" when given "main".
73
+ #
74
+ # TODO: Make this work with nested slices when given slash-delimited slice names like
75
+ # "parent/child", which should look for "slices/parent/slices/child".
76
+ #
77
+ # This method makes two checks for the slice root (building off both `app.root` as well
78
+ # as `fs`). This is entirely to account for how we test commands, with most tests using
79
+ # an in-memory `fs` adapter, any files created via which will be invisible to the `app`,
80
+ # which doesn't know about the `fs`.
81
+ #
82
+ # FIXME: It would be better to find a way for this to make one check only. An ideal
83
+ # approach would be to use the slice_name to find actual slice registered within
84
+ # `app.slices`. To do this, we'd probably need to stop testing with an in-memory `fs`
85
+ # here.
86
+ def detect_slice_root(slice_name)
87
+ slice_root_in_fs = fs.join("slices", inflector.underscore(slice_name))
88
+ return slice_root_in_fs if fs.exist?(slice_root_in_fs)
89
+
90
+ app.root.join("slices", inflector.underscore(slice_name))
54
91
  end
55
92
  end
56
93
  end