hanami-cli 2.1.0 → 2.2.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +18 -13
  3. data/CHANGELOG.md +21 -0
  4. data/Gemfile +4 -2
  5. data/README.md +11 -7
  6. data/docker-compose.yml +9 -0
  7. data/hanami-cli.gemspec +2 -2
  8. data/lib/hanami/cli/command.rb +1 -1
  9. data/lib/hanami/cli/commands/app/assets/command.rb +1 -1
  10. data/lib/hanami/cli/commands/app/command.rb +2 -16
  11. data/lib/hanami/cli/commands/app/db/command.rb +116 -0
  12. data/lib/hanami/cli/commands/app/db/create.rb +19 -11
  13. data/lib/hanami/cli/commands/app/db/drop.rb +19 -10
  14. data/lib/hanami/cli/commands/app/db/migrate.rb +19 -13
  15. data/lib/hanami/cli/commands/app/db/prepare.rb +42 -0
  16. data/lib/hanami/cli/commands/app/db/seed.rb +11 -22
  17. data/lib/hanami/cli/commands/app/db/structure/dump.rb +30 -7
  18. data/lib/hanami/cli/commands/app/db/structure/load.rb +52 -0
  19. data/lib/hanami/cli/commands/app/db/utils/database.rb +68 -73
  20. data/lib/hanami/cli/commands/app/db/utils/mysql.rb +2 -2
  21. data/lib/hanami/cli/commands/app/db/utils/postgres.rb +38 -19
  22. data/lib/hanami/cli/commands/app/db/utils/sqlite.rb +58 -10
  23. data/lib/hanami/cli/commands/app/db/version.rb +12 -9
  24. data/lib/hanami/cli/commands/app/generate/action.rb +4 -3
  25. data/lib/hanami/cli/commands/app/generate/command.rb +49 -0
  26. data/lib/hanami/cli/commands/app/generate/component.rb +49 -0
  27. data/lib/hanami/cli/commands/app/generate/migration.rb +27 -0
  28. data/lib/hanami/cli/commands/app/generate/operation.rb +26 -0
  29. data/lib/hanami/cli/commands/app/generate/part.rb +1 -1
  30. data/lib/hanami/cli/commands/app/generate/relation.rb +35 -0
  31. data/lib/hanami/cli/commands/app/generate/repo.rb +46 -0
  32. data/lib/hanami/cli/commands/app/generate/slice.rb +20 -3
  33. data/lib/hanami/cli/commands/app/generate/struct.rb +27 -0
  34. data/lib/hanami/cli/commands/app/install.rb +1 -1
  35. data/lib/hanami/cli/commands/app/middleware.rb +1 -1
  36. data/lib/hanami/cli/commands/app/server.rb +2 -2
  37. data/lib/hanami/cli/commands/app.rb +21 -2
  38. data/lib/hanami/cli/commands/gem/new.rb +78 -14
  39. data/lib/hanami/cli/errors.rb +28 -0
  40. data/lib/hanami/cli/generators/app/action/slice_action.erb +1 -1
  41. data/lib/hanami/cli/generators/app/action_context.rb +5 -13
  42. data/lib/hanami/cli/generators/app/component/component.erb +8 -0
  43. data/lib/hanami/cli/generators/app/component/slice_component.erb +8 -0
  44. data/lib/hanami/cli/generators/app/component.rb +61 -0
  45. data/lib/hanami/cli/generators/app/component_context.rb +82 -0
  46. data/lib/hanami/cli/generators/app/migration.rb +69 -0
  47. data/lib/hanami/cli/generators/app/operation.rb +48 -0
  48. data/lib/hanami/cli/generators/app/part_context.rb +5 -21
  49. data/lib/hanami/cli/generators/app/relation.rb +44 -0
  50. data/lib/hanami/cli/generators/app/repo.rb +40 -0
  51. data/lib/hanami/cli/generators/app/ruby_file_writer.rb +149 -0
  52. data/lib/hanami/cli/generators/app/slice/operation.erb +7 -0
  53. data/lib/hanami/cli/generators/app/slice/relation.erb +8 -0
  54. data/lib/hanami/cli/generators/app/slice/{slice.erb → repo.erb} +3 -1
  55. data/lib/hanami/cli/generators/app/slice/struct.erb +8 -0
  56. data/lib/hanami/cli/generators/app/slice.rb +14 -6
  57. data/lib/hanami/cli/generators/app/slice_context.rb +9 -2
  58. data/lib/hanami/cli/generators/app/struct.rb +39 -0
  59. data/lib/hanami/cli/generators/app/view_context.rb +4 -16
  60. data/lib/hanami/cli/generators/constants.rb +39 -0
  61. data/lib/hanami/cli/generators/context.rb +48 -0
  62. data/lib/hanami/cli/generators/gem/app/action.erb +3 -0
  63. data/lib/hanami/cli/generators/gem/app/env.erb +4 -0
  64. data/lib/hanami/cli/generators/gem/app/gemfile.erb +11 -0
  65. data/lib/hanami/cli/generators/gem/app/gitignore.erb +1 -1
  66. data/lib/hanami/cli/generators/gem/app/operation.erb +13 -0
  67. data/lib/hanami/cli/generators/gem/app/relation.erb +10 -0
  68. data/lib/hanami/cli/generators/gem/app/repo.erb +10 -0
  69. data/lib/hanami/cli/generators/gem/app/struct.erb +10 -0
  70. data/lib/hanami/cli/generators/gem/app.rb +19 -0
  71. data/lib/hanami/cli/ruby_file_generator.rb +123 -0
  72. data/lib/hanami/cli/version.rb +1 -1
  73. metadata +39 -16
  74. data/lib/hanami/cli/commands/app/db/create_migration.rb +0 -32
  75. data/lib/hanami/cli/commands/app/db/reset.rb +0 -28
  76. data/lib/hanami/cli/commands/app/db/rollback.rb +0 -81
  77. data/lib/hanami/cli/commands/app/db/sample_data.rb +0 -42
  78. data/lib/hanami/cli/commands/app/db/setup.rb +0 -26
  79. data/lib/hanami/cli/commands/app/db/utils/database_config.rb +0 -60
  80. data/lib/hanami/cli/generators/app/slice/repository.erb +0 -10
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module CLI
5
+ module Commands
6
+ module App
7
+ module DB
8
+ # @api private
9
+ module Structure
10
+ # @api private
11
+ class Load < DB::Command
12
+ STRUCTURE_PATH = File.join("config", "db", "structure.sql").freeze
13
+ private_constant :STRUCTURE_PATH
14
+
15
+ desc "Loads database from config/db/structure.sql file"
16
+
17
+ # @api private
18
+ def call(app: false, slice: nil, command_exit: method(:exit), **)
19
+ exit_codes = []
20
+
21
+ databases(app: app, slice: slice).each do |database|
22
+ structure_path = database.slice.root.join(STRUCTURE_PATH)
23
+ next unless structure_path.exist?
24
+
25
+ relative_structure_path = structure_path.relative_path_from(database.slice.app.root)
26
+
27
+ measure("#{database.name} structure loaded from #{relative_structure_path}") do
28
+ catch :load_failed do
29
+ result = database.exec_load_command
30
+ exit_codes << result.exit_code if result.respond_to?(:exit_code)
31
+
32
+ unless result.successful?
33
+ out.puts result.err
34
+ throw :load_failed, false
35
+ end
36
+
37
+ true
38
+ end
39
+ end
40
+ end
41
+
42
+ exit_codes.each do |code|
43
+ break command_exit.(code) if code > 0
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "database_config"
3
+ require "uri"
4
4
 
5
5
  module Hanami
6
6
  module CLI
@@ -9,15 +9,14 @@ module Hanami
9
9
  module DB
10
10
  module Utils
11
11
  # @api private
12
+ # @since 2.2.0
12
13
  class Database
13
- # @api private
14
- attr_reader :app
14
+ MIGRATIONS_DIR = "config/db/migrate"
15
+ private_constant :MIGRATIONS_DIR
15
16
 
16
- # @api private
17
- attr_reader :config
18
-
19
- # @api private
20
- SCHEME_MAP = {
17
+ DATABASE_CLASS_RESOLVER = Hash.new { |_, key|
18
+ raise "#{key} is not a supported db scheme"
19
+ }.update(
21
20
  "sqlite" => -> {
22
21
  require_relative("sqlite")
23
22
  Sqlite
@@ -34,84 +33,66 @@ module Hanami
34
33
  require_relative("mysql")
35
34
  Mysql
36
35
  }
37
- }.freeze
38
-
39
- # @api private
40
- def self.[](app)
41
- database_url =
42
- if app.key?(:settings) && app[:settings].respond_to?(:database_url)
43
- app[:settings].database_url
44
- else
45
- ENV.fetch("DATABASE_URL")
46
- end
36
+ ).freeze
47
37
 
48
- config = DatabaseConfig.new(database_url)
38
+ def self.[](slice, system_call:)
39
+ provider = slice.container.providers[:db]
40
+ raise "this is not a db slice" unless provider
49
41
 
50
- resolver = SCHEME_MAP.fetch(config.db_type) do
51
- raise "#{config.db_type} is not a supported db scheme"
52
- end
42
+ database_scheme = provider.source.database_url.then { URI(_1).scheme }
43
+ database_class = DATABASE_CLASS_RESOLVER[database_scheme].call
44
+ database_class.new(slice: slice, system_call: system_call)
45
+ end
46
+
47
+ attr_reader :slice
53
48
 
54
- klass = resolver.()
49
+ attr_reader :system_call
55
50
 
56
- klass.new(app: app, config: config)
51
+ def initialize(slice:, system_call:)
52
+ @slice = slice
53
+ @system_call = system_call
57
54
  end
58
55
 
59
- # @api private
60
- def initialize(app:, config:)
61
- @app = app
62
- @config = config
56
+ def name
57
+ database_uri.path.sub(%r{^/}, "")
63
58
  end
64
59
 
65
- # @api private
66
- def create_command
67
- raise Hanami::CLI::NotImplementedError
60
+ def database_url
61
+ slice.container.providers[:db].source.database_url
68
62
  end
69
63
 
70
- # @api private
71
- def drop_command
72
- raise Hanami::CLI::NotImplementedError
64
+ def database_uri
65
+ @database_uri ||= URI(database_url)
73
66
  end
74
67
 
75
- # @api private
76
- def dump_command
77
- raise Hanami::CLI::NotImplementedError
68
+ def gateway
69
+ slice["db.config"].gateways[:default]
78
70
  end
79
71
 
80
- # @api private
81
- def load_command
82
- raise Hanami::CLI::NotImplementedError
72
+ def connection
73
+ gateway.connection
83
74
  end
84
75
 
85
- # @api private
86
- def root_path
87
- app.root
76
+ def exec_create_command
77
+ raise Hanami::CLI::NotImplementedError
88
78
  end
89
79
 
90
- # @api private
91
- def rom_config
92
- @rom_config ||=
93
- begin
94
- app.prepare(:persistence)
95
- app.container["persistence.config"]
96
- end
80
+ def exec_drop_command
81
+ raise Hanami::CLI::NotImplementedError
97
82
  end
98
83
 
99
- # @api private
100
- def name
101
- config.db_name
84
+ def exists?
85
+ raise Hanami::CLI::NotImplementedError
102
86
  end
103
87
 
104
- # @api private
105
- def gateway
106
- rom_config.gateways[:default]
88
+ def exec_dump_command
89
+ raise Hanami::CLI::NotImplementedError
107
90
  end
108
91
 
109
- # @api private
110
- def connection
111
- gateway.connection
92
+ def exec_load_command
93
+ raise Hanami::CLI::NotImplementedError
112
94
  end
113
95
 
114
- # @api private
115
96
  def run_migrations(**options)
116
97
  require "rom/sql"
117
98
  ROM::SQL.with_gateway(gateway) do
@@ -119,24 +100,19 @@ module Hanami
119
100
  end
120
101
  end
121
102
 
122
- # @api private
123
103
  def migrator
124
- @migrator ||=
125
- begin
126
- require "rom/sql"
127
- ROM::SQL::Migration::Migrator.new(connection, path: File.join(root_path, "db/migrate"))
128
- end
129
- end
104
+ @migrator ||= begin
105
+ slice.prepare :db
130
106
 
131
- # @api private
132
- def applied_migrations
133
- sequel_migrator.applied_migrations
107
+ require "rom/sql"
108
+ ROM::SQL::Migration::Migrator.new(connection, path: migrations_path)
109
+ end
134
110
  end
135
111
 
136
- private
137
-
138
112
  def sequel_migrator
139
113
  @sequel_migrator ||= begin
114
+ slice.prepare :db
115
+
140
116
  require "sequel"
141
117
  Sequel.extension :migration
142
118
 
@@ -147,8 +123,27 @@ module Hanami
147
123
  end
148
124
  end
149
125
 
126
+ def applied_migrations
127
+ sequel_migrator.applied_migrations
128
+ end
129
+
150
130
  def migrations_path
151
- File.join(root_path, "db/migrate")
131
+ slice.root.join(MIGRATIONS_DIR)
132
+ end
133
+
134
+ def migrations_dir?
135
+ migrations_path.directory?
136
+ end
137
+
138
+ def structure_file
139
+ slice.root.join("config/db/structure.sql")
140
+ end
141
+
142
+ def schema_migrations_sql_dump
143
+ sql = +"INSERT INTO schema_migrations (filename) VALUES\n"
144
+ sql << applied_migrations.map { |v| "('#{v}')" }.join(",\n")
145
+ sql << ";"
146
+ sql
152
147
  end
153
148
  end
154
149
  end
@@ -16,12 +16,12 @@ module Hanami
16
16
  end
17
17
 
18
18
  # @api private
19
- def dump_command
19
+ def exec_dump_command
20
20
  raise Hanami::CLI::NotImplementedError
21
21
  end
22
22
 
23
23
  # @api private
24
- def load_command
24
+ def exec_load_command
25
25
  raise Hanami::CLI::NotImplementedError
26
26
  end
27
27
  end
@@ -11,49 +11,68 @@ module Hanami
11
11
  module DB
12
12
  module Utils
13
13
  # @api private
14
+ # @since 2.2.0
14
15
  class Postgres < Database
15
16
  # @api private
16
- def create_command
17
- existing_stdout, status = Open3.capture2(cli_env_vars, "psql -t -c '\\l #{escaped_name}'")
17
+ # @since 2.2.0
18
+ def exec_create_command
19
+ return true if exists?
18
20
 
19
- return true if status.success? && existing_stdout.include?(escaped_name)
20
-
21
- system(cli_env_vars, "createdb #{escaped_name}")
21
+ system_call.call("createdb #{escaped_name}", env: cli_env_vars)
22
22
  end
23
23
 
24
24
  # @api private
25
- def drop_command
26
- system(cli_env_vars, "dropdb #{escaped_name}")
25
+ # @since 2.2.0
26
+ def exec_drop_command
27
+ return true unless exists?
28
+
29
+ system_call.call("dropdb #{escaped_name}", env: cli_env_vars)
27
30
  end
28
31
 
29
32
  # @api private
30
- def dump_command
31
- system(cli_env_vars, "pg_dump --schema-only --no-owner #{escaped_name} > #{dump_file}")
33
+ # @since 2.2.0
34
+ def exists?
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?
32
37
  end
33
38
 
34
39
  # @api private
35
- def load_command
36
- raise "Not Implemented Yet"
40
+ # @since 2.2.0
41
+ def exec_dump_command
42
+ system_call.call(
43
+ "pg_dump --schema-only --no-privileges --no-owner --file #{structure_file} #{escaped_name}",
44
+ env: cli_env_vars
45
+ )
37
46
  end
38
47
 
39
48
  # @api private
49
+ # @since 2.2.0
50
+ def exec_load_command
51
+ system_call.call(
52
+ "psql --set ON_ERROR_STOP=1 --quiet --no-psqlrc --output #{File::NULL} --file #{structure_file} #{escaped_name}",
53
+ env: cli_env_vars
54
+ )
55
+ end
56
+
40
57
  def escaped_name
41
58
  Shellwords.escape(name)
42
59
  end
43
60
 
44
- # @api private
45
61
  def cli_env_vars
46
62
  @cli_env_vars ||= {}.tap do |vars|
47
- vars["PGHOST"] = config.host.to_s
48
- vars["PGPORT"] = config.port.to_s if config.port
49
- vars["PGUSER"] = config.user.to_s if config.user
50
- vars["PGPASSWORD"] = config.pass.to_s if config.pass
63
+ vars["PGHOST"] = database_uri.host.to_s if database_uri.host
64
+ vars["PGPORT"] = database_uri.port.to_s if database_uri.port
65
+ vars["PGUSER"] = database_uri.user.to_s if database_uri.user
66
+ vars["PGPASSWORD"] = database_uri.password.to_s if database_uri.password
51
67
  end
52
68
  end
53
69
 
54
- # @api private
55
- def dump_file
56
- "#{root_path}/db/structure.sql"
70
+ def schema_migrations_sql_dump
71
+ search_path = slice["db.gateway"].connection
72
+ .fetch("SHOW search_path").to_a.first
73
+ .fetch(:search_path)
74
+
75
+ +"SET search_path TO #{search_path};\n\n" << super
57
76
  end
58
77
  end
59
78
  end
@@ -9,32 +9,80 @@ module Hanami
9
9
  module DB
10
10
  module Utils
11
11
  # @api private
12
+ # @since 2.2.0
12
13
  class Sqlite < Database
13
14
  # @api private
14
- def create_command
15
- rom_config
16
- true
15
+ # @since 2.2.0
16
+ Failure = Struct.new(:err) do
17
+ def successful?
18
+ false
19
+ end
20
+
21
+ def exit_code
22
+ 1
23
+ end
24
+ end
25
+
26
+ # @api private
27
+ # @since 2.2.0
28
+ def exec_create_command
29
+ return true if exists?
30
+
31
+ FileUtils.mkdir_p(File.dirname(file_path))
32
+
33
+ system_call.call(%(sqlite3 #{file_path} "VACUUM;"))
17
34
  end
18
35
 
19
36
  # @api private
20
- def drop_command
21
- file_path.unlink
37
+ # @since 2.2.0
38
+ def exec_drop_command
39
+ begin
40
+ File.unlink(file_path) if exists?
41
+ rescue => e
42
+ # Mimic a system_call result
43
+ return Failure.new(e.message)
44
+ end
45
+
22
46
  true
23
47
  end
24
48
 
25
49
  # @api private
26
- def dump_command
27
- raise Hanami::CLI::NotImplementedError
50
+ # @since 2.2.0
51
+ def exists?
52
+ File.exist?(file_path)
53
+ end
54
+
55
+ # @api private
56
+ # @since 2.2.0
57
+ def exec_dump_command
58
+ system_call.call(%(sqlite3 #{file_path} ".schema --indent --nosys" > #{structure_file}))
28
59
  end
29
60
 
30
61
  # @api private
31
- def load_command
32
- raise Hanami::CLI::NotImplementedError
62
+ # @since 2.2.0
63
+ def exec_load_command
64
+ system_call.call("sqlite3 #{file_path} < #{structure_file}")
33
65
  end
34
66
 
35
67
  # @api private
68
+ # @since 2.2.0
69
+ def name
70
+ # Sequel expects sqlite:// URIs to operate the same as file:// URIs: 2 slashes for
71
+ # a relative path, 3 for an absolute path. In the case of 2 slashes, the first part
72
+ # of the path is considered by Ruby's `URI` as the `#host`.
73
+ @name ||= "#{database_uri.host}#{database_uri.path}"
74
+ end
75
+
76
+ private
77
+
36
78
  def file_path
37
- @file_path ||= Pathname("#{root_path}#{config.uri.path}").realpath
79
+ @file_path ||= begin
80
+ if File.absolute_path?(name)
81
+ name
82
+ else
83
+ slice.app.root.join(name).to_s
84
+ end
85
+ end
38
86
  end
39
87
  end
40
88
  end
@@ -1,24 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../app/command"
4
-
5
3
  module Hanami
6
4
  module CLI
7
5
  module Commands
8
6
  module App
9
7
  module DB
10
8
  # @api private
11
- class Version < App::Command
9
+ class Version < DB::Command
12
10
  desc "Print schema version"
13
11
 
14
- option :target, desc: "Target migration number", aliases: ["-t"]
15
-
16
12
  # @api private
17
- def call(target: nil, **) # rubocop:disable Lint/UnusedMethodArgument
18
- migration = database.applied_migrations.last
19
- version = migration ? File.basename(migration, ".*") : "not available"
13
+ def call(app: false, slice: nil, **)
14
+ databases(app: app, slice: slice).each do |database|
15
+ unless database.migrations_dir?
16
+ out.puts "=> Cannot find version for slice #{database.slice.slice_name.to_s.inspect}: missing config/db/migrate/ dir"
17
+ return
18
+ end
19
+
20
+ migration = database.applied_migrations.last
21
+ version = migration ? File.basename(migration, ".*") : "not available"
20
22
 
21
- out.puts "=> current schema version is #{version}"
23
+ out.puts "=> #{database.name} current schema version is #{version}"
24
+ end
22
25
  end
23
26
  end
24
27
  end
@@ -32,13 +32,13 @@ module Hanami
32
32
  option \
33
33
  :skip_view,
34
34
  required: false,
35
- type: :boolean,
35
+ type: :flag,
36
36
  default: DEFAULT_SKIP_VIEW,
37
37
  desc: "Skip view and template generation"
38
38
  option \
39
39
  :skip_tests,
40
40
  required: false,
41
- type: :boolean,
41
+ type: :flag,
42
42
  default: DEFAULT_SKIP_TESTS,
43
43
  desc: "Skip test generation"
44
44
  option :slice, required: false, desc: "Slice name"
@@ -85,7 +85,8 @@ module Hanami
85
85
  skip_view: DEFAULT_SKIP_VIEW,
86
86
  skip_tests: DEFAULT_SKIP_TESTS, # rubocop:disable Lint/UnusedMethodArgument
87
87
  slice: nil,
88
- context: nil, **
88
+ context: nil,
89
+ **
89
90
  )
90
91
  slice = inflector.underscore(Shellwords.shellescape(slice)) if slice
91
92
  name = naming.action_name(name)
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/inflector"
4
+ require "dry/files"
5
+ require "shellwords"
6
+ require_relative "../../../naming"
7
+ require_relative "../../../errors"
8
+
9
+ module Hanami
10
+ module CLI
11
+ module Commands
12
+ module App
13
+ module Generate
14
+ # @since 2.2.0
15
+ # @api private
16
+ class Command < App::Command
17
+ option :slice, required: false, desc: "Slice name"
18
+
19
+ attr_reader :generator
20
+ private :generator
21
+
22
+ # @since 2.2.0
23
+ # @api private
24
+ def initialize(
25
+ fs:,
26
+ inflector:,
27
+ **opts
28
+ )
29
+ super
30
+ @generator = generator_class.new(fs: fs, inflector: inflector, out: out)
31
+ end
32
+
33
+ def generator_class
34
+ # Must be implemented by subclasses, with class that takes:
35
+ # fs:, inflector:, out:
36
+ end
37
+
38
+ # @since 2.2.0
39
+ # @api private
40
+ def call(name:, slice: nil, **)
41
+ normalized_slice = inflector.underscore(slice) if slice
42
+ generator.call(app.namespace, name, normalized_slice)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/inflector"
4
+ require "dry/files"
5
+ require "shellwords"
6
+ module Hanami
7
+ module CLI
8
+ module Commands
9
+ module App
10
+ module Generate
11
+ # @api private
12
+ # @since 2.2.0
13
+ class Component < App::Command
14
+ argument :name, required: true, desc: "Component name"
15
+ option :slice, required: false, desc: "Slice name"
16
+
17
+ example [
18
+ %(operations.create_user (MyApp::Operations::CreateUser)),
19
+ %(operations.user.create (MyApp::Operations::Create::User)),
20
+ %(operations.create_user --slice=admin (Admin::Operations::CreateUser)),
21
+ %(Operations::CreateUser (MyApp::Operations::CreateUser)),
22
+ ]
23
+ attr_reader :generator
24
+ private :generator
25
+
26
+ # @api private
27
+ # @since 2.2.0
28
+ def initialize(
29
+ fs:, inflector:,
30
+ generator: Generators::App::Component.new(fs: fs, inflector: inflector),
31
+ **opts
32
+ )
33
+ @generator = generator
34
+ super(fs: fs, inflector: inflector, **opts)
35
+ end
36
+
37
+ # @api private
38
+ # @since 2.2.0
39
+ def call(name:, slice: nil, **)
40
+ slice = inflector.underscore(Shellwords.shellescape(slice)) if slice
41
+
42
+ generator.call(app.namespace, name, slice)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module CLI
5
+ module Commands
6
+ module App
7
+ module Generate
8
+ # @since 2.2.0
9
+ # @api private
10
+ class Migration < Command
11
+ argument :name, required: true, desc: "Migration name"
12
+
13
+ example [
14
+ %(create_posts),
15
+ %(add_published_at_to_posts),
16
+ %(create_users --slice=admin),
17
+ ]
18
+
19
+ def generator_class
20
+ Generators::App::Migration
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module CLI
5
+ module Commands
6
+ module App
7
+ module Generate
8
+ # @since 2.2.0
9
+ # @api private
10
+ class Operation < Command
11
+ argument :name, required: true, desc: "Operation name"
12
+
13
+ example [
14
+ %(books.add (MyApp::Books::Add)),
15
+ %(books.add --slice=admin (Admin::Books::Add)),
16
+ ]
17
+
18
+ def generator_class
19
+ Generators::App::Operation
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end