Skip to content

Commit d8837ec

Browse files
authored
Add Hanami::CLI::RubyFileGenerator, convert Operation to use it instead of ERB (#186)
* Add dry-operation to default Gemfile * Add base Operation class, based on dry-operation * Fix view spec * Add Operation generators * Add empty `call` method definition * Remove ostruct * Allow slash separator for generator * Allow slash separator for generator * Rename module to admin * Remove newlines in generated files By adding new templates for un-nested operations * Remove input as default args * Remove Operations namespace, generate in app/ or slices/SLICE_NAME/ * Prevent generating operation without namespace * Revert "Prevent generating operation without namespace" This reverts commit a5bd2f3. * Add recommendation to add namespace to operations * Change examples * Switch to outputting directly, remove Files#recommend * Add Hanami::CLI::RubyFileGenerator * x.x.x => 2.2.0 * x.x.x => 2.2.0 * Include Dry::Monads[:result] in base Action * Add .module tests * Convert top-level app operation to use RubyFileGenerator * Convert nested app operation to use RubyFileGenerator * Support slash separators * Convert top-level slice operation to use RubyFileGenerator * Remove OperationContext * Remove namespaces instance variable * Refactor to variables * Remove last temporary instance variable * Refactor * More refactoring, for clarity * Rename variable for clarity * Rename helper method * Simplify RubyFileGenerator, support older versions * Convert Operation generator to use simplified RubyFileGenerator * Remove un-used errors * Refactor * Older kwargs forwarding style * Refactor * Rename variable * Add explanatory comment Add dry-monads include for slice base action * Fix base slice action * Remove un-used ERB templates * Remove OperationContext * Ternary over and/or * Fix missing 'end' from bad merge * Fix namespace recommendation * Extract App::Generate::Command * Specify full name, to use App::Command * Use constants file * Move class methods above initialize * Use constants file * Add yard comments * Revert "Use constants file" This reverts commit 303f502. Would need to namespace it and we may want to this to standalone so keeping it here. It's just two little spaces anyway * Fix indent to be two spaces
1 parent 1409ad9 commit d8837ec

13 files changed

+525
-163
lines changed

lib/hanami/cli/command.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def self.new(
2727
inflector: Dry::Inflector.new,
2828
**opts
2929
)
30-
super(out: out, err: err, fs: fs, inflector: inflector, **opts)
30+
super
3131
end
3232

3333
# Returns a new command.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
argument :name, required: true, desc: "Name"
18+
option :slice, required: false, desc: "Slice name"
19+
20+
attr_reader :generator
21+
private :generator
22+
23+
# @since 2.2.0
24+
# @api private
25+
def initialize(
26+
fs:,
27+
inflector:,
28+
generator_class: nil,
29+
**opts
30+
)
31+
raise "Provide a generator class (that takes fs and inflector)" if generator_class.nil?
32+
33+
super(fs: fs, inflector: inflector, **opts)
34+
@generator = generator_class.new(fs: fs, inflector: inflector, out: out)
35+
end
36+
37+
# @since 2.2.0
38+
# @api private
39+
def call(name:, slice: nil, **)
40+
normalized_slice = inflector.underscore(Shellwords.shellescape(slice)) if slice
41+
generator.call(app.namespace, name, normalized_slice)
42+
end
43+
end
44+
end
45+
end
46+
end
47+
end
48+
end

lib/hanami/cli/commands/app/generate/operation.rb

+3-21
Original file line numberDiff line numberDiff line change
@@ -13,34 +13,16 @@ module App
1313
module Generate
1414
# @since 2.2.0
1515
# @api private
16-
class Operation < App::Command
17-
argument :name, required: true, desc: "Operation name"
18-
option :slice, required: false, desc: "Slice name"
19-
16+
class Operation < Generate::Command
2017
example [
2118
%(books.add (MyApp::Books::Add)),
2219
%(books.add --slice=admin (Admin::Books::Add)),
2320
]
24-
attr_reader :generator
25-
private :generator
26-
27-
# @since 2.2.0
28-
# @api private
29-
def initialize(
30-
fs:, inflector:,
31-
generator: Generators::App::Operation.new(fs: fs, inflector: inflector),
32-
**opts
33-
)
34-
super(fs: fs, inflector: inflector, **opts)
35-
@generator = generator
36-
end
3721

3822
# @since 2.2.0
3923
# @api private
40-
def call(name:, slice: nil, **)
41-
slice = inflector.underscore(Shellwords.shellescape(slice)) if slice
42-
43-
generator.call(app.namespace, name, slice)
24+
def initialize(**opts)
25+
super(generator_class: Generators::App::Operation, **opts)
4426
end
4527
end
4628
end

lib/hanami/cli/commands/app/generate/slice.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module App
1212
module Generate
1313
# @since 2.0.0
1414
# @api private
15-
class Slice < Command
15+
class Slice < App::Command
1616
argument :name, required: true, desc: "The slice name"
1717
option :url, required: false, type: :string, desc: "The slice URL prefix"
1818

lib/hanami/cli/generators/app/operation.rb

+58-31
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require "erb"
44
require "dry/files"
5+
require_relative "../constants"
56
require_relative "../../errors"
67

78
module Hanami
@@ -21,54 +22,80 @@ def initialize(fs:, inflector:, out: $stdout)
2122

2223
# @since 2.2.0
2324
# @api private
24-
def call(app, key, slice)
25-
context = OperationContext.new(inflector, app, slice, key)
25+
def call(app_namespace, key, slice)
26+
operation_name = key.split(KEY_SEPARATOR)[-1]
27+
local_namespaces = key.split(KEY_SEPARATOR)[..-2]
28+
container_namespace = slice || app_namespace
2629

27-
if slice
28-
generate_for_slice(context, slice)
29-
else
30-
generate_for_app(context)
31-
end
30+
raise_missing_slice_error_if_missing(slice) if slice
31+
print_namespace_recommendation(operation_name) if local_namespaces.none?
32+
33+
directory = directory(slice, local_namespaces: local_namespaces)
34+
path = fs.join(directory, "#{operation_name}.rb")
35+
fs.mkdir(directory)
36+
37+
file_contents = class_definition(
38+
operation_name: operation_name,
39+
container_namespace: container_namespace,
40+
local_namespaces: local_namespaces,
41+
)
42+
fs.write(path, file_contents)
3243
end
3344

3445
private
3546

3647
attr_reader :fs, :inflector, :out
3748

38-
def generate_for_slice(context, slice)
39-
slice_directory = fs.join("slices", slice)
40-
raise MissingSliceError.new(slice) unless fs.directory?(slice_directory)
49+
def directory(slice = nil, local_namespaces:)
50+
base = if slice
51+
fs.join("slices", slice)
52+
else
53+
fs.join("app")
54+
end
4155

42-
if context.namespaces.any?
43-
fs.mkdir(directory = fs.join(slice_directory, context.namespaces))
44-
fs.write(fs.join(directory, "#{context.name}.rb"), t("nested_slice_operation.erb", context))
56+
if local_namespaces.any?
57+
fs.join(base, local_namespaces)
4558
else
46-
fs.mkdir(directory = fs.join(slice_directory))
47-
fs.write(fs.join(directory, "#{context.name}.rb"), t("top_level_slice_operation.erb", context))
48-
out.puts(" Note: We generated a top-level operation. To generate into a directory, add a namespace: `my_namespace.#{context.name}`")
59+
fs.join(base)
4960
end
5061
end
5162

52-
def generate_for_app(context)
53-
if context.namespaces.any?
54-
fs.mkdir(directory = fs.join("app", context.namespaces))
55-
fs.write(fs.join(directory, "#{context.name}.rb"), t("nested_app_operation.erb", context))
56-
else
57-
fs.mkdir(directory = fs.join("app"))
58-
out.puts(" Note: We generated a top-level operation. To generate into a directory, add a namespace: `my_namespace.#{context.name}`")
59-
fs.write(fs.join(directory, "#{context.name}.rb"), t("top_level_app_operation.erb", context))
60-
end
63+
def class_definition(operation_name:, container_namespace:, local_namespaces:)
64+
container_module = normalize(container_namespace)
65+
66+
modules = local_namespaces
67+
.map { normalize(_1) }
68+
.compact
69+
.prepend(container_module)
70+
71+
parent_class = [container_module, "Operation"].join("::")
72+
73+
RubyFileGenerator.class(
74+
normalize(operation_name),
75+
parent_class: parent_class,
76+
modules: modules,
77+
body: ["def call", "end"],
78+
header: ["# frozen_string_literal: true"],
79+
)
6180
end
6281

63-
def template(path, context)
64-
require "erb"
82+
def normalize(name)
83+
inflector.camelize(name).gsub(/[^\p{Alnum}]/, "")
84+
end
6585

66-
ERB.new(
67-
File.read(__dir__ + "/operation/#{path}")
68-
).result(context.ctx)
86+
def print_namespace_recommendation(operation_name)
87+
out.puts(
88+
" Note: We generated a top-level operation. " \
89+
"To generate into a directory, add a namespace: `my_namespace.#{operation_name}`"
90+
)
6991
end
7092

71-
alias_method :t, :template
93+
def raise_missing_slice_error_if_missing(slice)
94+
if slice
95+
slice_directory = fs.join("slices", slice)
96+
raise MissingSliceError.new(slice) unless fs.directory?(slice_directory)
97+
end
98+
end
7299
end
73100
end
74101
end

lib/hanami/cli/generators/app/operation/nested_app_operation.erb

-10
This file was deleted.

lib/hanami/cli/generators/app/operation/nested_slice_operation.erb

-10
This file was deleted.

lib/hanami/cli/generators/app/operation/top_level_app_operation.erb

-8
This file was deleted.

lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb

-8
This file was deleted.

lib/hanami/cli/generators/app/operation_context.rb

-71
This file was deleted.

0 commit comments

Comments
 (0)