-
-
Notifications
You must be signed in to change notification settings - Fork 31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Hanami::CLI::RubyFileGenerator, convert Operation to use it instead of ERB #186
Changes from all commits
d3e5e78
888c388
24d3d6b
bc2670a
1c37df3
a3f6a03
d93dfc8
649bdcb
f74519f
0f9f814
663abc6
3b72feb
0f81a5c
a5bd2f3
eb391ca
1023225
6a3c32a
8dc3de4
7059e7e
0dfdf7d
8f90b33
8e62aa3
f0e0994
ed47e5e
4769bd6
8e5c575
8effa30
06f2750
79699f0
3879144
6f6fce3
30c2a94
da093d4
40237de
f0cf827
2dff130
36c7b3b
1c47e7e
2a4f0b6
0223bea
8e458ce
d4e13bd
f9221f0
5f5887b
e9e7051
a051c7d
d0d02c7
145f7ee
b76942c
26a665b
b07cf35
23aace2
f649975
5239824
511b6a0
fffd123
54dd28b
303f502
e824e5d
94c92e1
66d0c80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# frozen_string_literal: true | ||
|
||
require "dry/inflector" | ||
require "dry/files" | ||
require "shellwords" | ||
require_relative "../../../naming" | ||
require_relative "../../../errors" | ||
|
||
module Hanami | ||
module CLI | ||
module Commands | ||
module App | ||
module Generate | ||
# @since 2.2.0 | ||
# @api private | ||
class Command < App::Command | ||
argument :name, required: true, desc: "Name" | ||
option :slice, required: false, desc: "Slice name" | ||
|
||
attr_reader :generator | ||
private :generator | ||
|
||
# @since 2.2.0 | ||
# @api private | ||
def initialize( | ||
fs:, | ||
inflector:, | ||
generator_class: nil, | ||
**opts | ||
) | ||
raise "Provide a generator class (that takes fs and inflector)" if generator_class.nil? | ||
|
||
super(fs: fs, inflector: inflector, **opts) | ||
@generator = generator_class.new(fs: fs, inflector: inflector, out: out) | ||
end | ||
|
||
# @since 2.2.0 | ||
# @api private | ||
def call(name:, slice: nil, **) | ||
normalized_slice = inflector.underscore(Shellwords.shellescape(slice)) if slice | ||
generator.call(app.namespace, name, normalized_slice) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,34 +13,16 @@ module App | |
module Generate | ||
# @since 2.2.0 | ||
# @api private | ||
class Operation < App::Command | ||
argument :name, required: true, desc: "Operation name" | ||
option :slice, required: false, desc: "Slice name" | ||
|
||
class Operation < Generate::Command | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I love how tidy this class has become! |
||
example [ | ||
%(books.add (MyApp::Books::Add)), | ||
%(books.add --slice=admin (Admin::Books::Add)), | ||
] | ||
attr_reader :generator | ||
private :generator | ||
|
||
# @since 2.2.0 | ||
# @api private | ||
def initialize( | ||
fs:, inflector:, | ||
generator: Generators::App::Operation.new(fs: fs, inflector: inflector), | ||
**opts | ||
) | ||
super(fs: fs, inflector: inflector, **opts) | ||
@generator = generator | ||
end | ||
|
||
# @since 2.2.0 | ||
# @api private | ||
def call(name:, slice: nil, **) | ||
slice = inflector.underscore(Shellwords.shellescape(slice)) if slice | ||
|
||
generator.call(app.namespace, name, slice) | ||
def initialize(**opts) | ||
super(generator_class: Generators::App::Operation, **opts) | ||
end | ||
end | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
|
||
require "erb" | ||
require "dry/files" | ||
require_relative "../constants" | ||
require_relative "../../errors" | ||
|
||
module Hanami | ||
|
@@ -21,54 +22,80 @@ def initialize(fs:, inflector:, out: $stdout) | |
|
||
# @since 2.2.0 | ||
# @api private | ||
def call(app, key, slice) | ||
context = OperationContext.new(inflector, app, slice, key) | ||
def call(app_namespace, key, slice) | ||
operation_name = key.split(KEY_SEPARATOR)[-1] | ||
local_namespaces = key.split(KEY_SEPARATOR)[..-2] | ||
container_namespace = slice || app_namespace | ||
|
||
if slice | ||
generate_for_slice(context, slice) | ||
else | ||
generate_for_app(context) | ||
end | ||
raise_missing_slice_error_if_missing(slice) if slice | ||
print_namespace_recommendation(operation_name) if local_namespaces.none? | ||
|
||
directory = directory(slice, local_namespaces: local_namespaces) | ||
path = fs.join(directory, "#{operation_name}.rb") | ||
fs.mkdir(directory) | ||
|
||
file_contents = class_definition( | ||
operation_name: operation_name, | ||
container_namespace: container_namespace, | ||
local_namespaces: local_namespaces, | ||
) | ||
fs.write(path, file_contents) | ||
end | ||
|
||
private | ||
|
||
attr_reader :fs, :inflector, :out | ||
|
||
def generate_for_slice(context, slice) | ||
slice_directory = fs.join("slices", slice) | ||
raise MissingSliceError.new(slice) unless fs.directory?(slice_directory) | ||
def directory(slice = nil, local_namespaces:) | ||
base = if slice | ||
fs.join("slices", slice) | ||
else | ||
fs.join("app") | ||
end | ||
Comment on lines
+50
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we can pass an actual slice instance into this, we could use our new
This returns the appropriate value for both the app as well as slices, so it means we no longer need (Again, this would be another great refactor for post-merge, I don't want to hold anything up!) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extracted to #202 |
||
|
||
if context.namespaces.any? | ||
fs.mkdir(directory = fs.join(slice_directory, context.namespaces)) | ||
fs.write(fs.join(directory, "#{context.name}.rb"), t("nested_slice_operation.erb", context)) | ||
if local_namespaces.any? | ||
fs.join(base, local_namespaces) | ||
else | ||
fs.mkdir(directory = fs.join(slice_directory)) | ||
fs.write(fs.join(directory, "#{context.name}.rb"), t("top_level_slice_operation.erb", context)) | ||
out.puts(" Note: We generated a top-level operation. To generate into a directory, add a namespace: `my_namespace.#{context.name}`") | ||
fs.join(base) | ||
end | ||
end | ||
|
||
def generate_for_app(context) | ||
if context.namespaces.any? | ||
fs.mkdir(directory = fs.join("app", context.namespaces)) | ||
fs.write(fs.join(directory, "#{context.name}.rb"), t("nested_app_operation.erb", context)) | ||
else | ||
fs.mkdir(directory = fs.join("app")) | ||
out.puts(" Note: We generated a top-level operation. To generate into a directory, add a namespace: `my_namespace.#{context.name}`") | ||
fs.write(fs.join(directory, "#{context.name}.rb"), t("top_level_app_operation.erb", context)) | ||
end | ||
def class_definition(operation_name:, container_namespace:, local_namespaces:) | ||
container_module = normalize(container_namespace) | ||
|
||
modules = local_namespaces | ||
.map { normalize(_1) } | ||
.compact | ||
.prepend(container_module) | ||
|
||
parent_class = [container_module, "Operation"].join("::") | ||
|
||
RubyFileGenerator.class( | ||
normalize(operation_name), | ||
parent_class: parent_class, | ||
modules: modules, | ||
body: ["def call", "end"], | ||
header: ["# frozen_string_literal: true"], | ||
) | ||
end | ||
|
||
def template(path, context) | ||
require "erb" | ||
def normalize(name) | ||
inflector.camelize(name).gsub(/[^\p{Alnum}]/, "") | ||
end | ||
|
||
ERB.new( | ||
File.read(__dir__ + "/operation/#{path}") | ||
).result(context.ctx) | ||
def print_namespace_recommendation(operation_name) | ||
out.puts( | ||
" Note: We generated a top-level operation. " \ | ||
"To generate into a directory, add a namespace: `my_namespace.#{operation_name}`" | ||
) | ||
end | ||
|
||
alias_method :t, :template | ||
def raise_missing_slice_error_if_missing(slice) | ||
if slice | ||
slice_directory = fs.join("slices", slice) | ||
raise MissingSliceError.new(slice) unless fs.directory?(slice_directory) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
|
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like
generator_class
is better suited to being a class attribute at this point.Now that we're asking for a class for the generator, and taking care of initializing it with our standard options (fs, inflector, out), the generator itself no longer needs to be injectable for the purposes of testing, and beyond that there's really no need to be able to switch the generator at all.
This way, the subclasses of
CLI::Commands::App::Generate
can just specify their generator class inter class body, and not even have to worry about overriding#initialize
.Note: I think this would be fine to experiment with in a follow-up PR rather than blocking this one any further :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Already addressed in #199, I forgot to backport it here :)