hanami-cli 2.2.0 → 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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -0
  3. data/CHANGELOG.md +43 -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/install.rb +12 -0
  21. data/lib/hanami/cli/commands/app/server.rb +0 -1
  22. data/lib/hanami/cli/commands/app.rb +1 -0
  23. data/lib/hanami/cli/commands/gem/new.rb +37 -7
  24. data/lib/hanami/cli/errors.rb +26 -0
  25. data/lib/hanami/cli/files.rb +20 -6
  26. data/lib/hanami/cli/generators/app/action.rb +78 -101
  27. data/lib/hanami/cli/generators/app/component.rb +11 -33
  28. data/lib/hanami/cli/generators/app/migration.rb +1 -1
  29. data/lib/hanami/cli/generators/app/operation.rb +4 -5
  30. data/lib/hanami/cli/generators/app/part.rb +42 -65
  31. data/lib/hanami/cli/generators/app/relation.rb +4 -5
  32. data/lib/hanami/cli/generators/app/repo.rb +3 -5
  33. data/lib/hanami/cli/generators/app/ruby_class_file.rb +32 -0
  34. data/lib/hanami/cli/generators/app/ruby_file.rb +128 -0
  35. data/lib/hanami/cli/generators/app/ruby_module_file.rb +28 -0
  36. data/lib/hanami/cli/generators/app/slice.rb +130 -37
  37. data/lib/hanami/cli/generators/app/struct.rb +3 -4
  38. data/lib/hanami/cli/generators/app/view.rb +40 -45
  39. data/lib/hanami/cli/generators/context.rb +6 -0
  40. data/lib/hanami/cli/generators/gem/app/assets.js +14 -13
  41. data/lib/hanami/cli/generators/gem/app/dev +1 -1
  42. data/lib/hanami/cli/generators/gem/app/gemfile.erb +5 -0
  43. data/lib/hanami/cli/generators/gem/app/gitignore.erb +3 -1
  44. data/lib/hanami/cli/generators/gem/app/rakefile.erb +3 -0
  45. data/lib/hanami/cli/generators/gem/app/readme.erb +14 -0
  46. data/lib/hanami/cli/generators/gem/app.rb +40 -37
  47. data/lib/hanami/cli/ruby_file_generator.rb +17 -8
  48. data/lib/hanami/cli/server.rb +15 -1
  49. data/lib/hanami/cli/version.rb +1 -1
  50. data/lib/hanami/console/context.rb +5 -0
  51. metadata +35 -41
  52. data/lib/hanami/cli/generators/app/action/action.erb +0 -17
  53. data/lib/hanami/cli/generators/app/action/slice_action.erb +0 -17
  54. data/lib/hanami/cli/generators/app/action/slice_template.html.erb +0 -1
  55. data/lib/hanami/cli/generators/app/action/slice_view.erb +0 -10
  56. data/lib/hanami/cli/generators/app/action/template.erb +0 -0
  57. data/lib/hanami/cli/generators/app/action/template.html.erb +0 -1
  58. data/lib/hanami/cli/generators/app/action/view.erb +0 -10
  59. data/lib/hanami/cli/generators/app/action_context.rb +0 -90
  60. data/lib/hanami/cli/generators/app/component/component.erb +0 -8
  61. data/lib/hanami/cli/generators/app/component/slice_component.erb +0 -8
  62. data/lib/hanami/cli/generators/app/component_context.rb +0 -82
  63. data/lib/hanami/cli/generators/app/part/app_base_part.erb +0 -9
  64. data/lib/hanami/cli/generators/app/part/app_part.erb +0 -13
  65. data/lib/hanami/cli/generators/app/part/slice_base_part.erb +0 -9
  66. data/lib/hanami/cli/generators/app/part/slice_part.erb +0 -13
  67. data/lib/hanami/cli/generators/app/part_context.rb +0 -82
  68. data/lib/hanami/cli/generators/app/ruby_file_writer.rb +0 -151
  69. data/lib/hanami/cli/generators/app/slice/action.erb +0 -7
  70. data/lib/hanami/cli/generators/app/slice/app_css.erb +0 -5
  71. data/lib/hanami/cli/generators/app/slice/app_js.erb +0 -1
  72. data/lib/hanami/cli/generators/app/slice/app_layout.erb +0 -18
  73. data/lib/hanami/cli/generators/app/slice/helpers.erb +0 -10
  74. data/lib/hanami/cli/generators/app/slice/keep.erb +0 -0
  75. data/lib/hanami/cli/generators/app/slice/operation.erb +0 -7
  76. data/lib/hanami/cli/generators/app/slice/relation.erb +0 -8
  77. data/lib/hanami/cli/generators/app/slice/repo.erb +0 -8
  78. data/lib/hanami/cli/generators/app/slice/routes.erb +0 -3
  79. data/lib/hanami/cli/generators/app/slice/struct.erb +0 -8
  80. data/lib/hanami/cli/generators/app/slice/view.erb +0 -7
  81. data/lib/hanami/cli/generators/app/slice_context.rb +0 -72
  82. data/lib/hanami/cli/generators/app/view/app_template.html.erb +0 -1
  83. data/lib/hanami/cli/generators/app/view/app_view.erb +0 -10
  84. data/lib/hanami/cli/generators/app/view/slice_template.html.erb +0 -1
  85. data/lib/hanami/cli/generators/app/view/slice_view.erb +0 -10
  86. data/lib/hanami/cli/generators/app/view_context.rb +0 -88
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "erb"
4
3
  require "dry/files"
4
+ require_relative "../constants"
5
5
  require_relative "../../errors"
6
6
 
7
7
  # rubocop:disable Metrics/ParameterLists
@@ -14,20 +14,25 @@ module Hanami
14
14
  class Action
15
15
  # @since 2.0.0
16
16
  # @api private
17
- def initialize(fs:, inflector:)
17
+ def initialize(fs:, inflector:, out: $stdout)
18
18
  @fs = fs
19
19
  @inflector = inflector
20
+ @out = out
21
+ @view_generator = Generators::App::View.new(
22
+ fs: fs,
23
+ inflector: inflector,
24
+ out: out
25
+ )
20
26
  end
21
27
 
22
28
  # @since 2.0.0
23
29
  # @api private
24
- def call(app, controller, action, url, http, format, skip_view, skip_route, slice, context: nil)
25
- context ||= ActionContext.new(inflector, app, slice, controller, action)
26
- if slice
27
- generate_for_slice(controller, action, url, http, format, skip_view, skip_route, slice, context)
28
- else
29
- generate_for_app(controller, action, url, http, format, skip_view, skip_route, context)
30
- end
30
+ def call(key:, namespace:, base_path:, url_path:, http_method:, skip_view:, skip_route:)
31
+ insert_route(key:, namespace:, url_path:, http_method:) unless skip_route
32
+
33
+ generate_action(key:, namespace:, base_path:, include_placeholder_body: skip_view)
34
+
35
+ generate_view(key:, namespace:, base_path:) unless skip_view
31
36
  end
32
37
 
33
38
  private
@@ -67,86 +72,76 @@ module Hanami
67
72
  PATH_SEPARATOR = "/"
68
73
  private_constant :PATH_SEPARATOR
69
74
 
70
- attr_reader :fs
71
-
72
- attr_reader :inflector
73
-
74
- # rubocop:disable Metrics/AbcSize
75
- def generate_for_slice(controller, action, url, http, format, skip_view, skip_route, slice, context)
76
- slice_directory = fs.join("slices", slice)
77
- raise MissingSliceError.new(slice) unless fs.directory?(slice_directory)
78
-
79
- if generate_route?(skip_route)
80
- fs.inject_line_at_block_bottom(
81
- fs.join("config", "routes.rb"),
82
- slice_matcher(slice),
83
- route(controller, action, url, http)
84
- )
85
- end
86
-
87
- fs.mkdir(directory = fs.join(slice_directory, "actions", controller))
88
- fs.write(fs.join(directory, "#{action}.rb"), t("slice_action.erb", context))
75
+ attr_reader :fs, :inflector, :out, :view_generator
89
76
 
90
- if generate_view?(skip_view, action, directory)
91
- fs.mkdir(directory = fs.join(slice_directory, "views", controller))
92
- fs.write(fs.join(directory, "#{action}.rb"), t("slice_view.erb", context))
77
+ # @api private
78
+ # @since 2.2.2
79
+ def insert_route(key:, namespace:, url_path:, http_method:)
80
+ routes_location = fs.join("config", "routes.rb")
81
+ route = route_definition(key:, url_path:, http_method:)
93
82
 
94
- fs.mkdir(directory = fs.join(slice_directory, "templates", controller))
95
- fs.write(fs.join(directory, "#{action}.#{format}.erb"),
96
- t(template_with_format_ext("slice_template", format), context))
83
+ if namespace == Hanami.app.namespace
84
+ fs.inject_line_at_class_bottom(routes_location, "class Routes", route)
85
+ else
86
+ slice_matcher = /slice[[:space:]]*:#{namespace}/
87
+ fs.inject_line_at_block_bottom(routes_location, slice_matcher, route)
97
88
  end
98
89
  end
99
90
 
100
- def generate_for_app(controller, action, url, http, format, skip_view, skip_route, context)
101
- if generate_route?(skip_route)
102
- fs.inject_line_at_class_bottom(
103
- fs.join("config", "routes.rb"),
104
- "class Routes",
105
- route(controller, action, url, http)
106
- )
107
- end
108
-
109
- fs.mkdir(directory = fs.join("app", "actions", controller))
110
- fs.write(fs.join(directory, "#{action}.rb"), t("action.erb", context))
91
+ # @api private
92
+ # @since 2.2.2
93
+ def generate_action(key:, namespace:, base_path:, include_placeholder_body:)
94
+ RubyClassFile.new(
95
+ fs: fs,
96
+ inflector: inflector,
97
+ namespace: namespace,
98
+ key: inflector.underscore(key),
99
+ base_path: base_path,
100
+ parent_class_name: "#{inflector.camelize(namespace)}::Action",
101
+ extra_namespace: "Actions",
102
+ body: [
103
+ "def handle(request, response)",
104
+ (" response.body = self.class.name" if include_placeholder_body),
105
+ "end"
106
+ ].compact
107
+ ).create
108
+ end
111
109
 
112
- view = action
113
- view_directory = fs.join("app", "views", controller)
110
+ # @api private
111
+ # @since 2.2.2
112
+ def generate_view(key:, namespace:, base_path:)
113
+ *controller_name_parts, action_name = key.split(KEY_SEPARATOR)
114
114
 
115
- if generate_view?(skip_view, view, view_directory)
116
- fs.mkdir(view_directory)
117
- fs.write(fs.join(view_directory, "#{view}.rb"), t("view.erb", context))
115
+ view_directory = fs.join(base_path, "views", controller_name_parts)
118
116
 
119
- fs.mkdir(template_directory = fs.join("app", "templates", controller))
120
- fs.write(fs.join(template_directory, "#{view}.#{format}.erb"),
121
- t(template_with_format_ext("template", format), context))
117
+ if generate_view?(action_name, view_directory)
118
+ view_generator.call(
119
+ key: key,
120
+ namespace: namespace,
121
+ base_path: base_path,
122
+ )
122
123
  end
123
124
  end
124
- # rubocop:enable Metrics/AbcSize
125
-
126
- def slice_matcher(slice)
127
- /slice[[:space:]]*:#{slice}/
128
- end
129
-
130
- def route(controller, action, url, http)
131
- %(#{route_http(action,
132
- http)} "#{route_url(controller, action, url)}", to: "#{controller.join('.')}.#{action}")
133
- end
134
125
 
135
126
  # @api private
136
- # @since 2.1.0
137
- def generate_view?(skip_view, view, directory)
138
- return false if skip_view
139
- return generate_restful_view?(view, directory) if rest_view?(view)
127
+ # @since 2.2.2
128
+ def route_definition(key:, url_path:, http_method:)
129
+ *controller_name_parts, action_name = key.split(KEY_SEPARATOR)
130
+
131
+ method = route_http(action_name, http_method)
132
+ path = route_url(controller_name_parts, action_name, url_path)
140
133
 
141
- true
134
+ %(#{method} "#{path}", to: "#{key}")
142
135
  end
143
136
 
144
137
  # @api private
145
- # @since 2.2.0
146
- def generate_route?(skip_route)
147
- return false if skip_route
148
-
149
- true
138
+ # @since 2.1.0
139
+ def generate_view?(action_name, directory)
140
+ if rest_view?(action_name)
141
+ generate_restful_view?(action_name, directory)
142
+ else
143
+ true
144
+ end
150
145
  end
151
146
 
152
147
  # @api private
@@ -169,40 +164,22 @@ module Hanami
169
164
  RESTFUL_COUNTERPART_VIEWS.fetch(view, nil)
170
165
  end
171
166
 
172
- def template_with_format_ext(name, format)
173
- ext =
174
- case format.to_sym
175
- when :html
176
- ".html.erb"
177
- else
178
- ".erb"
179
- end
180
-
181
- "#{name}#{ext}"
182
- end
183
-
184
- def template(path, context)
185
- require "erb"
186
-
187
- ERB.new(
188
- File.read(__dir__ + "/action/#{path}"), trim_mode: "-",
189
- ).result(context.ctx)
190
- end
191
-
192
- alias_method :t, :template
193
-
194
- def route_url(controller, action, url)
167
+ # @api private
168
+ # @since 2.1.0
169
+ def route_url(controller, action, url_path)
195
170
  action = ROUTE_RESTFUL_URL_SUFFIXES.fetch(action) { [action] }
196
- url ||= "#{PATH_SEPARATOR}#{(controller + action).join(PATH_SEPARATOR)}"
171
+ url_path ||= "#{PATH_SEPARATOR}#{(controller + action).join(PATH_SEPARATOR)}"
197
172
 
198
- CLI::URL.call(url)
173
+ CLI::URL.call(url_path)
199
174
  end
200
175
 
201
- def route_http(action, http)
202
- result = (http ||= ROUTE_RESTFUL_HTTP_METHODS.fetch(action, ROUTE_DEFAULT_HTTP_METHOD)).downcase
176
+ # @api private
177
+ # @since 2.1.0
178
+ def route_http(action, http_method)
179
+ result = (http_method ||= ROUTE_RESTFUL_HTTP_METHODS.fetch(action, ROUTE_DEFAULT_HTTP_METHOD)).downcase
203
180
 
204
181
  unless ROUTE_HTTP_METHODS.include?(result)
205
- raise UnknownHTTPMethodError.new(http)
182
+ raise UnknownHTTPMethodError.new(http_method)
206
183
  end
207
184
 
208
185
  result
@@ -12,49 +12,27 @@ module Hanami
12
12
  class Component
13
13
  # @api private
14
14
  # @since 2.2.0
15
- def initialize(fs:, inflector:)
15
+ def initialize(fs:, inflector:, out: $stdout)
16
16
  @fs = fs
17
17
  @inflector = inflector
18
+ @out = out
18
19
  end
19
20
 
20
21
  # @api private
21
22
  # @since 2.2.0
22
- def call(app, key, slice)
23
- context = ComponentContext.new(inflector, app, slice, key)
24
-
25
- if slice
26
- generate_for_slice(context, slice)
27
- else
28
- generate_for_app(context)
29
- end
23
+ def call(key:, namespace:, base_path:)
24
+ RubyClassFile.new(
25
+ fs: fs,
26
+ inflector: inflector,
27
+ key: inflector.underscore(key),
28
+ namespace: namespace,
29
+ base_path: base_path,
30
+ ).create
30
31
  end
31
32
 
32
33
  private
33
34
 
34
- attr_reader :fs
35
-
36
- attr_reader :inflector
37
-
38
- def generate_for_slice(context, slice)
39
- slice_directory = fs.join("slices", slice)
40
- raise MissingSliceError.new(slice) unless fs.directory?(slice_directory)
41
-
42
- fs.mkdir(directory = fs.join(slice_directory, context.namespaces))
43
- fs.write(fs.join(directory, "#{context.underscored_name}.rb"), t("slice_component.erb", context))
44
- end
45
-
46
- def generate_for_app(context)
47
- fs.mkdir(directory = fs.join("app", context.namespaces))
48
- fs.write(fs.join(directory, "#{context.underscored_name}.rb"), t("component.erb", context))
49
- end
50
-
51
- def template(path, context)
52
- ERB.new(
53
- File.read(__dir__ + "/component/#{path}")
54
- ).result(context.ctx)
55
- end
56
-
57
- alias_method :t, :template
35
+ attr_reader :fs, :inflector, :out
58
36
  end
59
37
  end
60
38
  end
@@ -26,7 +26,7 @@ module Hanami
26
26
 
27
27
  path = fs.join(*[base_path, "config", "db", migrate_dir, file_name(name)].compact)
28
28
 
29
- fs.write(path, FILE_CONTENTS)
29
+ fs.create(path, FILE_CONTENTS)
30
30
  end
31
31
 
32
32
  private
@@ -20,16 +20,15 @@ module Hanami
20
20
  # @since 2.2.0
21
21
  # @api private
22
22
  def call(key:, namespace:, base_path:)
23
- RubyFileWriter.new(
23
+ RubyClassFile.new(
24
24
  fs: fs,
25
25
  inflector: inflector,
26
- ).call(
26
+ key: key,
27
27
  namespace: namespace,
28
28
  base_path: base_path,
29
- key: key,
30
- relative_parent_class: "Operation",
29
+ parent_class_name: "#{inflector.camelize(namespace)}::Operation",
31
30
  body: ["def call", "end"],
32
- )
31
+ ).create
33
32
 
34
33
  unless key.match?(KEY_SEPARATOR)
35
34
  out.puts(
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "erb"
4
4
  require "dry/files"
5
- require_relative "../../errors"
6
5
 
7
6
  module Hanami
8
7
  module CLI
@@ -13,87 +12,65 @@ module Hanami
13
12
  class Part
14
13
  # @since 2.1.0
15
14
  # @api private
16
- def initialize(fs:, inflector:)
15
+ def initialize(fs:, inflector:, out: $stdout)
17
16
  @fs = fs
18
17
  @inflector = inflector
18
+ @out = out
19
19
  end
20
20
 
21
21
  # @since 2.1.0
22
22
  # @api private
23
- def call(app, key, slice)
24
- context = PartContext.new(inflector, app, slice, key)
25
-
26
- if slice
27
- generate_for_slice(context, slice)
28
- else
29
- generate_for_app(context)
30
- end
23
+ def call(key:, namespace:, base_path:, **)
24
+ create_app_base_part_if_missing(key:, namespace:, base_path:)
25
+ create_slice_part_if_missing(key:, namespace:, base_path:) unless namespace == Hanami.app.namespace
26
+ create_generated_part(key:, namespace:, base_path:)
31
27
  end
32
28
 
33
29
  private
34
30
 
35
31
  # @since 2.1.0
36
32
  # @api private
37
- attr_reader :fs
38
-
39
- # @since 2.1.0
40
- # @api private
41
- attr_reader :inflector
42
-
43
- # @since 2.1.0
44
- # @api private
45
- def generate_for_slice(context, slice)
46
- slice_directory = fs.join("slices", slice)
47
- raise MissingSliceError.new(slice) unless fs.directory?(slice_directory)
48
-
49
- generate_base_part_for_app(context)
50
- generate_base_part_for_slice(context, slice)
51
-
52
- fs.mkdir(directory = fs.join(slice_directory, "views", "parts", *context.underscored_namespace))
53
- fs.write(fs.join(directory, "#{context.underscored_name}.rb"), t("slice_part.erb", context))
54
- end
55
-
56
- # @since 2.1.0
57
- # @api private
58
- def generate_for_app(context)
59
- generate_base_part_for_app(context)
60
-
61
- fs.mkdir(directory = fs.join("app", "views", "parts", *context.underscored_namespace))
62
- fs.write(fs.join(directory, "#{context.underscored_name}.rb"), t("app_part.erb", context))
63
- end
64
-
65
- # @since 2.1.0
66
- # @api private
67
- def generate_base_part_for_app(context)
68
- path = fs.join("app", "views", "part.rb")
69
- return if fs.exist?(path)
70
-
71
- fs.write(path, t("app_base_part.erb", context))
33
+ attr_reader :fs, :inflector, :out
34
+
35
+ def create_app_base_part_if_missing(key:, namespace:, base_path:)
36
+ return if fs.exist?(fs.join(base_path, "views", "part.rb"))
37
+
38
+ RubyClassFile.new(
39
+ fs: fs,
40
+ inflector: inflector,
41
+ namespace: Hanami.app.namespace,
42
+ key: "views.part",
43
+ base_path: APP_DIR,
44
+ parent_class_name: "Hanami::View::Part",
45
+ auto_register: false
46
+ ).create
72
47
  end
73
48
 
74
- # @since 2.1.0
75
- # @api private
76
- def generate_base_part_for_slice(context, slice)
77
- path = fs.join("slices", slice, "views", "part.rb")
78
- return if fs.exist?(path)
79
-
80
- fs.write(path, t("slice_base_part.erb", context))
49
+ def create_slice_part_if_missing(key:, namespace:, base_path:)
50
+ return if fs.exist?(fs.join(base_path, "views", "part.rb"))
51
+
52
+ RubyClassFile.new(
53
+ fs: fs,
54
+ inflector: inflector,
55
+ namespace: namespace,
56
+ key: "views.part",
57
+ base_path: base_path,
58
+ parent_class_name: "#{Hanami.app.namespace}::Views::Part",
59
+ auto_register: false
60
+ ).create
81
61
  end
82
62
 
83
- # @since 2.1.0
84
- # @api private
85
- def template(path, context)
86
- require "erb"
87
-
88
- ERB.new(
89
- File.read(__dir__ + "/part/#{path}"),
90
- trim_mode: "-"
91
- ).result(context.ctx)
63
+ def create_generated_part(key:, namespace:, base_path:)
64
+ RubyClassFile.new(
65
+ fs: fs,
66
+ inflector: inflector,
67
+ namespace: namespace,
68
+ key: inflector.underscore("views.parts.#{key}"),
69
+ base_path: base_path,
70
+ parent_class_name: "#{inflector.camelize(namespace)}::Views::Part",
71
+ auto_register: false
72
+ ).create
92
73
  end
93
-
94
- # @since 2.1.0
95
- # @api private
96
- alias_method :t, :template
97
74
  end
98
75
  end
99
76
  end
@@ -25,17 +25,16 @@ module Hanami
25
25
 
26
26
  body_content.prepend("gateway :#{gateway}") if gateway
27
27
 
28
- RubyFileWriter.new(
28
+ RubyClassFile.new(
29
29
  fs: fs,
30
30
  inflector: inflector,
31
- ).call(
32
- namespace: namespace,
33
31
  key: key,
32
+ namespace: namespace,
34
33
  base_path: base_path,
35
34
  extra_namespace: "Relations",
36
- relative_parent_class: "DB::Relation",
35
+ parent_class_name: "#{inflector.camelize(namespace)}::DB::Relation",
37
36
  body: body_content,
38
- )
37
+ ).create
39
38
  end
40
39
 
41
40
  private
@@ -18,17 +18,15 @@ module Hanami
18
18
  # @since 2.2.0
19
19
  # @api private
20
20
  def call(key:, namespace:, base_path:)
21
- RubyFileWriter.new(
21
+ RubyClassFile.new(
22
22
  fs: fs,
23
23
  inflector: inflector,
24
- ).call(
25
24
  key: key,
26
25
  namespace: namespace,
27
26
  base_path: base_path,
28
27
  extra_namespace: "Repos",
29
- relative_parent_class: "DB::Repo",
30
- body: [],
31
- )
28
+ parent_class_name: "#{inflector.camelize(namespace)}::DB::Repo",
29
+ ).create
32
30
  end
33
31
 
34
32
  private
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../constants"
4
+
5
+ module Hanami
6
+ module CLI
7
+ module Generators
8
+ module App
9
+ # @api private
10
+ class RubyClassFile < RubyFile
11
+ def initialize(parent_class_name: nil, **args)
12
+ super
13
+
14
+ @parent_class_name = parent_class_name
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :parent_class_name
20
+
21
+ def class_name
22
+ constant_name
23
+ end
24
+
25
+ def modules
26
+ namespace_modules
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../constants"
4
+
5
+ module Hanami
6
+ module CLI
7
+ module Generators
8
+ module App
9
+ # @api private
10
+ class RubyFile
11
+ def initialize(
12
+ fs:,
13
+ inflector:,
14
+ key:,
15
+ namespace:,
16
+ base_path:,
17
+ extra_namespace: nil,
18
+ auto_register: nil,
19
+ body: [],
20
+ **opts
21
+ )
22
+ @fs = fs
23
+ @inflector = inflector
24
+ @key = key
25
+ @namespace = namespace
26
+ @base_path = base_path
27
+ @extra_namespace = extra_namespace&.downcase
28
+ @auto_register = auto_register
29
+ @body = body
30
+ end
31
+
32
+ # @api private
33
+ def create
34
+ fs.create(path, file_contents)
35
+ end
36
+
37
+ # @api private
38
+ def write
39
+ fs.write(path, file_contents)
40
+ end
41
+
42
+ # @api private
43
+ def fully_qualified_name
44
+ inflector.camelize(
45
+ [namespace, extra_namespace, *key_parts].join("/"),
46
+ )
47
+ end
48
+
49
+ # @api private
50
+ def path
51
+ fs.join(directory, "#{key_parts.last}.rb")
52
+ end
53
+
54
+ private
55
+
56
+ # @api private
57
+ attr_reader(
58
+ :fs,
59
+ :inflector,
60
+ :key,
61
+ :base_path,
62
+ :namespace,
63
+ :extra_namespace,
64
+ :auto_register,
65
+ :body,
66
+ )
67
+
68
+ # @api private
69
+ def file_contents
70
+ RubyFileGenerator.new(
71
+ # These first three must be implemented by subclasses
72
+ class_name: class_name,
73
+ parent_class_name: parent_class_name,
74
+ modules: modules,
75
+ header: headers,
76
+ body: body
77
+ ).call
78
+ end
79
+
80
+ # @api private
81
+ def local_namespaces
82
+ Array(extra_namespace) + key_parts[..-2]
83
+ end
84
+
85
+ # @api private
86
+ def namespace_modules
87
+ [namespace, *local_namespaces]
88
+ .map { normalize(_1) }
89
+ .compact
90
+ end
91
+
92
+ # @api private
93
+ def directory
94
+ @directory ||= if local_namespaces.any?
95
+ fs.join(base_path, local_namespaces)
96
+ else
97
+ base_path
98
+ end
99
+ end
100
+
101
+ # @api private
102
+ def constant_name
103
+ normalize(key_parts.last)
104
+ end
105
+
106
+ # @api private
107
+ def headers
108
+ [
109
+ # Intentional ternary logic. Skip if nil, else 'true' or 'false'
110
+ ("# auto_register: #{auto_register}" unless auto_register.nil?),
111
+ "# frozen_string_literal: true",
112
+ ].compact
113
+ end
114
+
115
+ # @api private
116
+ def normalize(name)
117
+ inflector.camelize(name).gsub(/[^\p{Alnum}]/, "")
118
+ end
119
+
120
+ # @api private
121
+ def key_parts
122
+ key.split(KEY_SEPARATOR)
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end