hanami-cli 2.1.1 → 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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +18 -13
  3. data/CHANGELOG.md +15 -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/command.rb +2 -16
  10. data/lib/hanami/cli/commands/app/db/command.rb +116 -0
  11. data/lib/hanami/cli/commands/app/db/create.rb +19 -11
  12. data/lib/hanami/cli/commands/app/db/drop.rb +19 -10
  13. data/lib/hanami/cli/commands/app/db/migrate.rb +19 -13
  14. data/lib/hanami/cli/commands/app/db/prepare.rb +42 -0
  15. data/lib/hanami/cli/commands/app/db/seed.rb +11 -22
  16. data/lib/hanami/cli/commands/app/db/structure/dump.rb +30 -7
  17. data/lib/hanami/cli/commands/app/db/structure/load.rb +52 -0
  18. data/lib/hanami/cli/commands/app/db/utils/database.rb +68 -73
  19. data/lib/hanami/cli/commands/app/db/utils/mysql.rb +2 -2
  20. data/lib/hanami/cli/commands/app/db/utils/postgres.rb +38 -19
  21. data/lib/hanami/cli/commands/app/db/utils/sqlite.rb +58 -10
  22. data/lib/hanami/cli/commands/app/db/version.rb +12 -9
  23. data/lib/hanami/cli/commands/app/generate/action.rb +4 -3
  24. data/lib/hanami/cli/commands/app/generate/command.rb +49 -0
  25. data/lib/hanami/cli/commands/app/generate/component.rb +49 -0
  26. data/lib/hanami/cli/commands/app/generate/migration.rb +27 -0
  27. data/lib/hanami/cli/commands/app/generate/operation.rb +26 -0
  28. data/lib/hanami/cli/commands/app/generate/part.rb +1 -1
  29. data/lib/hanami/cli/commands/app/generate/relation.rb +35 -0
  30. data/lib/hanami/cli/commands/app/generate/repo.rb +46 -0
  31. data/lib/hanami/cli/commands/app/generate/slice.rb +20 -3
  32. data/lib/hanami/cli/commands/app/generate/struct.rb +27 -0
  33. data/lib/hanami/cli/commands/app/install.rb +1 -1
  34. data/lib/hanami/cli/commands/app/middleware.rb +1 -1
  35. data/lib/hanami/cli/commands/app/server.rb +2 -2
  36. data/lib/hanami/cli/commands/app.rb +21 -2
  37. data/lib/hanami/cli/commands/gem/new.rb +78 -14
  38. data/lib/hanami/cli/errors.rb +28 -0
  39. data/lib/hanami/cli/generators/app/action_context.rb +5 -13
  40. data/lib/hanami/cli/generators/app/component/component.erb +8 -0
  41. data/lib/hanami/cli/generators/app/component/slice_component.erb +8 -0
  42. data/lib/hanami/cli/generators/app/component.rb +61 -0
  43. data/lib/hanami/cli/generators/app/component_context.rb +82 -0
  44. data/lib/hanami/cli/generators/app/migration.rb +69 -0
  45. data/lib/hanami/cli/generators/app/operation.rb +48 -0
  46. data/lib/hanami/cli/generators/app/part_context.rb +5 -21
  47. data/lib/hanami/cli/generators/app/relation.rb +44 -0
  48. data/lib/hanami/cli/generators/app/repo.rb +40 -0
  49. data/lib/hanami/cli/generators/app/ruby_file_writer.rb +149 -0
  50. data/lib/hanami/cli/generators/app/slice/operation.erb +7 -0
  51. data/lib/hanami/cli/generators/app/slice/relation.erb +8 -0
  52. data/lib/hanami/cli/generators/app/slice/{slice.erb → repo.erb} +3 -1
  53. data/lib/hanami/cli/generators/app/slice/struct.erb +8 -0
  54. data/lib/hanami/cli/generators/app/slice.rb +14 -6
  55. data/lib/hanami/cli/generators/app/slice_context.rb +9 -2
  56. data/lib/hanami/cli/generators/app/struct.rb +39 -0
  57. data/lib/hanami/cli/generators/app/view_context.rb +4 -16
  58. data/lib/hanami/cli/generators/constants.rb +39 -0
  59. data/lib/hanami/cli/generators/context.rb +48 -0
  60. data/lib/hanami/cli/generators/gem/app/action.erb +3 -0
  61. data/lib/hanami/cli/generators/gem/app/env.erb +4 -0
  62. data/lib/hanami/cli/generators/gem/app/gemfile.erb +11 -0
  63. data/lib/hanami/cli/generators/gem/app/gitignore.erb +1 -1
  64. data/lib/hanami/cli/generators/gem/app/operation.erb +13 -0
  65. data/lib/hanami/cli/generators/gem/app/relation.erb +10 -0
  66. data/lib/hanami/cli/generators/gem/app/repo.erb +10 -0
  67. data/lib/hanami/cli/generators/gem/app/struct.erb +10 -0
  68. data/lib/hanami/cli/generators/gem/app.rb +19 -0
  69. data/lib/hanami/cli/ruby_file_generator.rb +123 -0
  70. data/lib/hanami/cli/version.rb +1 -1
  71. metadata +39 -16
  72. data/lib/hanami/cli/commands/app/db/create_migration.rb +0 -32
  73. data/lib/hanami/cli/commands/app/db/reset.rb +0 -28
  74. data/lib/hanami/cli/commands/app/db/rollback.rb +0 -81
  75. data/lib/hanami/cli/commands/app/db/sample_data.rb +0 -42
  76. data/lib/hanami/cli/commands/app/db/setup.rb +0 -26
  77. data/lib/hanami/cli/commands/app/db/utils/database_config.rb +0 -60
  78. data/lib/hanami/cli/generators/app/slice/repository.erb +0 -10
@@ -0,0 +1,46 @@
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 Repo < Command
17
+ argument :name, required: true, desc: "Repo name"
18
+
19
+ example [
20
+ %(books (MyApp::Repos::BooksRepo)),
21
+ %(books/drafts_repo (MyApp::Repos::Books::DraftsRepo)),
22
+ %(books --slice=admin (Admin::Repos::BooksRepo)),
23
+ ]
24
+
25
+ # @since 2.2.0
26
+ # @api private
27
+ def generator_class
28
+ Generators::App::Repo
29
+ end
30
+
31
+ # @since 2.2.0
32
+ # @api private
33
+ def call(name:, slice: nil, **opts)
34
+ normalized_name = if name.end_with?("_repo")
35
+ name
36
+ else
37
+ "#{inflector.singularize(name)}_repo"
38
+ end
39
+ super(name: normalized_name, slice: slice, **opts)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -12,10 +12,23 @@ module Hanami
12
12
  module Generate
13
13
  # @since 2.0.0
14
14
  # @api private
15
- class Slice < Command
15
+ class Slice < App::Command
16
16
  argument :name, required: true, desc: "The slice name"
17
17
  option :url, required: false, type: :string, desc: "The slice URL prefix"
18
18
 
19
+ # @since 2.2.0
20
+ # @api private
21
+ SKIP_DB_DEFAULT = false
22
+ private_constant :SKIP_DB_DEFAULT
23
+
24
+ # @since 2.2.0
25
+ # @api private
26
+ option :skip_db,
27
+ type: :boolean,
28
+ required: false,
29
+ default: SKIP_DB_DEFAULT,
30
+ desc: "Skip database"
31
+
19
32
  example [
20
33
  "admin # Admin slice (/admin URL prefix)",
21
34
  "users --url=/u # Users slice (/u URL prefix)",
@@ -34,14 +47,18 @@ module Hanami
34
47
 
35
48
  # @since 2.0.0
36
49
  # @api private
37
- def call(name:, url: nil, **)
50
+ def call(
51
+ name:,
52
+ url: nil,
53
+ skip_db: SKIP_DB_DEFAULT
54
+ )
38
55
  require "hanami/setup"
39
56
 
40
57
  app = inflector.underscore(Hanami.app.namespace)
41
58
  name = inflector.underscore(Shellwords.shellescape(name))
42
59
  url = sanitize_url_prefix(name, url)
43
60
 
44
- generator.call(app, name, url)
61
+ generator.call(app, name, url, skip_db: skip_db)
45
62
  end
46
63
 
47
64
  private
@@ -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 Struct < Command
11
+ argument :name, required: true, desc: "Struct name"
12
+
13
+ example [
14
+ %(book (MyApp::Structs::Book)),
15
+ %(book/published_book (MyApp::Structs::Book::PublishedBook)),
16
+ %(book --slice=admin (Admin::Structs::Book)),
17
+ ]
18
+
19
+ def generator_class
20
+ Generators::App::Struct
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -31,7 +31,7 @@ module Hanami
31
31
 
32
32
  # @since 2.1.0
33
33
  # @api private
34
- option :head, type: :boolean, desc: "Install head deps", default: DEFAULT_HEAD
34
+ option :head, type: :flag, desc: "Install head deps", default: DEFAULT_HEAD
35
35
 
36
36
  # @since 2.0.0
37
37
  # @api private
@@ -34,7 +34,7 @@ module Hanami
34
34
  private_constant :DEFAULT_WITH_ARGUMENTS
35
35
 
36
36
  option :with_arguments, default: DEFAULT_WITH_ARGUMENTS, required: false,
37
- desc: "Include inspected arguments", type: :boolean
37
+ desc: "Include inspected arguments", type: :flag
38
38
 
39
39
  example [
40
40
  "middleware # Print app Rack middleware stack",
@@ -41,8 +41,8 @@ module Hanami
41
41
  option :port, default: Hanami::Port::DEFAULT, required: false,
42
42
  desc: "The port to run the server on (falls back to the rack handler)"
43
43
  option :config, default: DEFAULT_CONFIG_PATH, required: false, desc: "Rack configuration file"
44
- option :debug, default: false, required: false, desc: "Turn on/off debug output", type: :boolean
45
- option :warn, default: false, required: false, desc: "Turn on/off warnings", type: :boolean
44
+ option :debug, default: false, required: false, desc: "Turn on/off debug output", type: :flag
45
+ option :warn, default: false, required: false, desc: "Turn on/off warnings", type: :flag
46
46
 
47
47
  # @since 2.0.0
48
48
  # @api private
@@ -27,11 +27,30 @@ module Hanami
27
27
  end
28
28
  end
29
29
 
30
+ if Hanami.bundled?("hanami-db")
31
+ register "db" do |db|
32
+ db.register "create", DB::Create
33
+ db.register "drop", DB::Drop
34
+ db.register "migrate", DB::Migrate
35
+ db.register "structure dump", DB::Structure::Dump
36
+ db.register "structure load", DB::Structure::Load
37
+ db.register "seed", DB::Seed
38
+ db.register "prepare", DB::Prepare
39
+ db.register "version", DB::Version
40
+ end
41
+ end
42
+
30
43
  register "generate", aliases: ["g"] do |prefix|
31
- prefix.register "slice", Generate::Slice
32
44
  prefix.register "action", Generate::Action
33
- prefix.register "view", Generate::View
45
+ prefix.register "component", Generate::Component
46
+ prefix.register "migration", Generate::Migration
47
+ prefix.register "operation", Generate::Operation
34
48
  prefix.register "part", Generate::Part
49
+ prefix.register "relation", Generate::Relation
50
+ prefix.register "repo", Generate::Repo
51
+ prefix.register "slice", Generate::Slice
52
+ prefix.register "struct", Generate::Struct
53
+ prefix.register "view", Generate::View
35
54
  end
36
55
  end
37
56
  end
@@ -25,6 +25,27 @@ module Hanami
25
25
  SKIP_ASSETS_DEFAULT = false
26
26
  private_constant :SKIP_ASSETS_DEFAULT
27
27
 
28
+ # @since 2.2.0
29
+ # @api private
30
+ SKIP_DB_DEFAULT = false
31
+ private_constant :SKIP_DB_DEFAULT
32
+
33
+ # @since 2.2.0
34
+ # @api private
35
+ DATABASE_SQLITE = "sqlite"
36
+
37
+ # @since 2.2.0
38
+ # @api private
39
+ DATABASE_POSTGRES = "postgres"
40
+
41
+ # @since 2.2.0
42
+ # @api private
43
+ DATABASE_MYSQL = "mysql"
44
+
45
+ # @since 2.2.0
46
+ # @api private
47
+ SUPPORTED_DATABASES = [DATABASE_SQLITE, DATABASE_POSTGRES, DATABASE_MYSQL].freeze
48
+
28
49
  desc "Generate a new Hanami app"
29
50
 
30
51
  # @since 2.0.0
@@ -33,28 +54,42 @@ module Hanami
33
54
 
34
55
  # @since 2.0.0
35
56
  # @api private
36
- option :skip_install, type: :boolean, required: false,
57
+ option :skip_install, type: :flag, required: false,
37
58
  default: SKIP_INSTALL_DEFAULT,
38
59
  desc: "Skip app installation (Bundler, third-party Hanami plugins)"
39
60
 
40
61
  # @since 2.1.0
41
62
  # @api private
42
- option :head, type: :boolean, required: false,
63
+ option :head, type: :flag, required: false,
43
64
  default: HEAD_DEFAULT,
44
65
  desc: "Use Hanami HEAD version (from GitHub `main` branches)"
45
66
 
46
67
  # @since 2.1.0
47
68
  # @api private
48
- option :skip_assets, type: :boolean, required: false,
69
+ option :skip_assets, type: :flag, required: false,
49
70
  default: SKIP_ASSETS_DEFAULT,
50
- desc: "Skip assets"
71
+ desc: "Skip including hanami-assets"
72
+
73
+ # @since 2.2.0
74
+ # @api private
75
+ option :skip_db, type: :flag, required: false,
76
+ default: SKIP_DB_DEFAULT,
77
+ desc: "Skip including hanami-db"
78
+
79
+ # @since 2.2.0
80
+ # @api private
81
+ option :database, type: :string, required: false,
82
+ default: DATABASE_SQLITE,
83
+ desc: "Database adapter (supported: sqlite, mysql, postgres)"
51
84
 
52
85
  # rubocop:disable Layout/LineLength
53
86
  example [
54
- "bookshelf # Generate a new Hanami app in `bookshelf/' directory, using `Bookshelf' namespace",
55
- "bookshelf --head # Generate a new Hanami app, using Hanami HEAD version from GitHub `main' branches",
56
- "bookshelf --skip-install # Generate a new Hanami app, but it skips Hanami installation",
57
- "bookshelf --skip-assets # Generate a new Hanami app without assets"
87
+ "bookshelf # Generate a new Hanami app in `bookshelf/' directory, using `Bookshelf' namespace",
88
+ "bookshelf --head # Generate a new Hanami app, using Hanami HEAD version from GitHub `main' branches",
89
+ "bookshelf --skip-install # Generate a new Hanami app, but it skips Hanami installation",
90
+ "bookshelf --skip-assets # Generate a new Hanami app without hanmai-assets",
91
+ "bookshelf --skip-db # Generate a new Hanami app without hanami-db",
92
+ "bookshelf --database={sqlite|postgres|mysql} # Generate a new Hanami app with a specified database (default: sqlite)",
58
93
  ]
59
94
  # rubocop:enable Layout/LineLength
60
95
 
@@ -75,20 +110,36 @@ module Hanami
75
110
  @system_call = system_call
76
111
  end
77
112
 
78
- # rubocop:enable Metrics/ParameterLists
79
-
80
- # rubocop:disable Metrics/AbcSize
113
+ # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
81
114
 
82
115
  # @since 2.0.0
83
116
  # @api private
84
- def call(app:, head: HEAD_DEFAULT, skip_install: SKIP_INSTALL_DEFAULT, skip_assets: SKIP_ASSETS_DEFAULT, **)
117
+ def call(
118
+ app:,
119
+ head: HEAD_DEFAULT,
120
+ skip_install: SKIP_INSTALL_DEFAULT,
121
+ skip_assets: SKIP_ASSETS_DEFAULT,
122
+ skip_db: SKIP_DB_DEFAULT,
123
+ database: nil
124
+ )
125
+ # rubocop:enable Metrics/ParameterLists
85
126
  app = inflector.underscore(app)
86
127
 
87
128
  raise PathAlreadyExistsError.new(app) if fs.exist?(app)
129
+ raise ConflictingOptionsError.new("--skip-db", "--database") if skip_db && database
130
+
131
+ normalized_database ||= normalize_database(database)
88
132
 
89
133
  fs.mkdir(app)
90
134
  fs.chdir(app) do
91
- context = Generators::Context.new(inflector, app, head: head, skip_assets: skip_assets)
135
+ context = Generators::Context.new(
136
+ inflector,
137
+ app,
138
+ head: head,
139
+ skip_assets: skip_assets,
140
+ skip_db: skip_db,
141
+ database: normalized_database
142
+ )
92
143
  generator.call(app, context: context) do
93
144
  if skip_install
94
145
  out.puts "Skipping installation, please enter `#{app}' directory and run `bundle exec hanami install'"
@@ -112,7 +163,7 @@ module Hanami
112
163
  end
113
164
  end
114
165
  end
115
- # rubocop:enable Metrics/AbcSize
166
+ # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
116
167
 
117
168
  private
118
169
 
@@ -120,6 +171,19 @@ module Hanami
120
171
  attr_reader :generator
121
172
  attr_reader :system_call
122
173
 
174
+ def normalize_database(database)
175
+ case database
176
+ when nil, "sqlite", "sqlite3"
177
+ DATABASE_SQLITE
178
+ when "mysql", "mysql2"
179
+ DATABASE_MYSQL
180
+ when "postgres", "postgresql", "pg"
181
+ DATABASE_POSTGRES
182
+ else
183
+ raise DatabaseNotSupportedError.new(database, SUPPORTED_DATABASES)
184
+ end
185
+ end
186
+
123
187
  def run_install_command!(head:)
124
188
  head_flag = head ? " --head" : ""
125
189
  bundler.exec("hanami install#{head_flag}").tap do |result|
@@ -91,5 +91,33 @@ module Hanami
91
91
  super("`#{scheme}' is not a supported db scheme")
92
92
  end
93
93
  end
94
+
95
+ # @since 2.2.0
96
+ # @api public
97
+ class DatabaseNotSupportedError < Error
98
+ def initialize(invalid_database, supported_databases)
99
+ super("`#{invalid_database}' is not a supported database. Supported databases are: #{supported_databases.join(', ')}")
100
+ end
101
+ end
102
+
103
+ # @since 2.2.0
104
+ # @api public
105
+ class ConflictingOptionsError < Error
106
+ def initialize(option1, option2)
107
+ super("`#{option1}' and `#{option2}' cannot be used together")
108
+ end
109
+ end
110
+
111
+ # @since 2.2.0
112
+ # @api public
113
+ class InvalidMigrationNameError < Error
114
+ def initialize(name)
115
+ super(<<~TEXT)
116
+ Invalid migration name: #{name}
117
+
118
+ Name must contain only letters, numbers, and underscores.
119
+ TEXT
120
+ end
121
+ end
94
122
  end
95
123
  end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "slice_context"
4
3
  require "dry/files/path"
4
+ require_relative "slice_context"
5
+ require_relative "../constants"
5
6
 
6
7
  module Hanami
7
8
  module CLI
@@ -52,7 +53,7 @@ module Hanami
52
53
  # @api private
53
54
  def module_controller_declaration
54
55
  controller.each_with_index.map do |token, i|
55
- "#{OFFSET}#{INDENTATION * i}module #{inflector.camelize(token)}"
56
+ "#{NESTED_OFFSET}#{INDENTATION * i}module #{inflector.camelize(token)}"
56
57
  end.join($/)
57
58
  end
58
59
 
@@ -60,14 +61,14 @@ module Hanami
60
61
  # @api private
61
62
  def module_controller_end
62
63
  controller.each_with_index.map do |_, i|
63
- "#{OFFSET}#{INDENTATION * i}end"
64
+ "#{NESTED_OFFSET}#{INDENTATION * i}end"
64
65
  end.reverse.join($/)
65
66
  end
66
67
 
67
68
  # @since 2.0.0
68
69
  # @api private
69
70
  def module_controller_offset
70
- "#{OFFSET}#{INDENTATION * controller.count}"
71
+ "#{NESTED_OFFSET}#{INDENTATION * controller.count}"
71
72
  end
72
73
 
73
74
  # @since 2.0.0
@@ -79,15 +80,6 @@ module Hanami
79
80
 
80
81
  private
81
82
 
82
- NAMESPACE_SEPARATOR = "::"
83
- private_constant :NAMESPACE_SEPARATOR
84
-
85
- INDENTATION = " "
86
- private_constant :INDENTATION
87
-
88
- OFFSET = INDENTATION * 2
89
- private_constant :OFFSET
90
-
91
83
  attr_reader :controller
92
84
 
93
85
  attr_reader :action
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= camelized_app_name %>
4
+ <%= module_namespace_declaration %>
5
+ <%= module_namespace_offset %>class <%= camelized_name %>
6
+ <%= module_namespace_offset %>end
7
+ <%= module_namespace_end %>
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= camelized_slice_name %>
4
+ <%= module_namespace_declaration %>
5
+ <%= module_namespace_offset %>class <%= camelized_name %>
6
+ <%= module_namespace_offset %>end
7
+ <%= module_namespace_end %>
8
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "dry/files"
5
+ module Hanami
6
+ module CLI
7
+ module Generators
8
+ module App
9
+ # @api private
10
+ # @since 2.2.0
11
+ class Component
12
+ # @api private
13
+ # @since 2.2.0
14
+ def initialize(fs:, inflector:)
15
+ @fs = fs
16
+ @inflector = inflector
17
+ end
18
+
19
+ # @api private
20
+ # @since 2.2.0
21
+ def call(app, key, slice)
22
+ context = ComponentContext.new(inflector, app, slice, key)
23
+
24
+ if slice
25
+ generate_for_slice(context, slice)
26
+ else
27
+ generate_for_app(context)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :fs
34
+
35
+ attr_reader :inflector
36
+
37
+ def generate_for_slice(context, slice)
38
+ slice_directory = fs.join("slices", slice)
39
+ raise MissingSliceError.new(slice) unless fs.directory?(slice_directory)
40
+
41
+ fs.mkdir(directory = fs.join(slice_directory, context.namespaces))
42
+ fs.write(fs.join(directory, "#{context.underscored_name}.rb"), t("slice_component.erb", context))
43
+ end
44
+
45
+ def generate_for_app(context)
46
+ fs.mkdir(directory = fs.join("app", context.namespaces))
47
+ fs.write(fs.join(directory, "#{context.underscored_name}.rb"), t("component.erb", context))
48
+ end
49
+
50
+ def template(path, context)
51
+ ERB.new(
52
+ File.read(__dir__ + "/component/#{path}")
53
+ ).result(context.ctx)
54
+ end
55
+
56
+ alias_method :t, :template
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../constants"
4
+
5
+ module Hanami
6
+ module CLI
7
+ module Generators
8
+ module App
9
+ class ComponentContext < SliceContext
10
+ # @api private
11
+ # @since 2.2.0
12
+ attr_reader :key
13
+
14
+ # @api private
15
+ # @since 2.2.0
16
+ def initialize(inflector, app, slice, key)
17
+ @key = key
18
+ super(inflector, app, slice, nil)
19
+ end
20
+
21
+ # @api private
22
+ # @since 2.2.0
23
+ def namespaces
24
+ @namespaces ||= key.split(MATCHER_PATTERN)[..-2].map { inflector.underscore(_1) }
25
+ end
26
+
27
+ # @api private
28
+ # @since 2.2.0
29
+ def name
30
+ @name ||= key.split(MATCHER_PATTERN)[-1]
31
+ end
32
+
33
+ # @api private
34
+ # @since 2.2.0
35
+ def camelized_namespace
36
+ namespaces.map { inflector.camelize(_1) }.join(NAMESPACE_SEPARATOR)
37
+ end
38
+
39
+ # @api private
40
+ # @since 2.2.0
41
+ def camelized_name
42
+ inflector.camelize(name)
43
+ end
44
+
45
+ # @api private
46
+ # @since 2.2.0
47
+ def underscored_namespace
48
+ namespaces.map { inflector.underscore(_1) }
49
+ end
50
+
51
+ # @api private
52
+ # @since 2.2.0
53
+ def underscored_name
54
+ inflector.underscore(name)
55
+ end
56
+
57
+ # @api private
58
+ # @since 2.2.0
59
+ def module_namespace_declaration
60
+ namespaces.each_with_index.map { |token, i|
61
+ "#{OFFSET}#{INDENTATION * i}module #{inflector.camelize(token)}"
62
+ }.join($/)
63
+ end
64
+
65
+ # @api private
66
+ # @since 2.2.0
67
+ def module_namespace_end
68
+ namespaces.each_with_index.map { |_, i|
69
+ "#{OFFSET}#{INDENTATION * i}end"
70
+ }.reverse.join($/)
71
+ end
72
+
73
+ # @api private
74
+ # @since 2.2.0
75
+ def module_namespace_offset
76
+ "#{OFFSET}#{INDENTATION * namespaces.count}"
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module CLI
5
+ module Generators
6
+ module App
7
+ # @since 2.2.0
8
+ # @api private
9
+ class Migration
10
+ # @since 2.2.0
11
+ # @api private
12
+ def initialize(fs:, inflector:, out: $stdout)
13
+ @fs = fs
14
+ @inflector = inflector
15
+ @out = out
16
+ end
17
+
18
+ # @since 2.2.0
19
+ # @api private
20
+ def call(_app_namespace, name, slice, **_opts)
21
+ normalized_name = inflector.underscore(name)
22
+ ensure_valid_name(normalized_name)
23
+
24
+ base = if slice
25
+ fs.join("slices", slice, "config", "db", "migrate")
26
+ else
27
+ fs.join("config", "db", "migrate")
28
+ end
29
+
30
+ path = fs.join(base, file_name(normalized_name))
31
+
32
+ fs.write(path, FILE_CONTENTS)
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :fs, :inflector, :out
38
+
39
+ VALID_NAME_REGEX = /^[_a-z0-9]+$/
40
+ private_constant :VALID_NAME_REGEX
41
+
42
+ def ensure_valid_name(name)
43
+ unless VALID_NAME_REGEX.match?(name.downcase)
44
+ raise InvalidMigrationNameError.new(name)
45
+ end
46
+ end
47
+
48
+ def file_name(name)
49
+ "#{Time.now.strftime(VERSION_FORMAT)}_#{name}.rb"
50
+ end
51
+
52
+ VERSION_FORMAT = "%Y%m%d%H%M%S"
53
+ private_constant :VERSION_FORMAT
54
+
55
+ FILE_CONTENTS = <<~RUBY
56
+ # frozen_string_literal: true
57
+
58
+ ROM::SQL.migration do
59
+ # Add your migration here.
60
+ #
61
+ # See https://sequel.jeremyevans.net/rdoc/files/doc/migration_rdoc.html for details.
62
+ end
63
+ RUBY
64
+ private_constant :FILE_CONTENTS
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end