hanami-cli 2.1.1 → 2.2.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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