Skip to content
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

Merged
merged 61 commits into from
Jul 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
d3e5e78
Add dry-operation to default Gemfile
cllns Jun 14, 2024
888c388
Add base Operation class, based on dry-operation
cllns Jun 14, 2024
24d3d6b
Fix view spec
cllns Jun 14, 2024
bc2670a
Add Operation generators
cllns Jun 15, 2024
1c37df3
Add empty `call` method definition
cllns Jun 15, 2024
a3f6a03
Remove ostruct
cllns Jun 15, 2024
d93dfc8
Merge branch 'main' into add-dry-operation
cllns Jun 17, 2024
649bdcb
Allow slash separator for generator
cllns Jun 17, 2024
f74519f
Allow slash separator for generator
cllns Jun 17, 2024
0f9f814
Rename module to admin
cllns Jun 17, 2024
663abc6
Remove newlines in generated files
cllns Jun 17, 2024
3b72feb
Remove input as default args
cllns Jun 19, 2024
0f81a5c
Remove Operations namespace, generate in app/ or slices/SLICE_NAME/
cllns Jun 19, 2024
a5bd2f3
Prevent generating operation without namespace
cllns Jun 19, 2024
eb391ca
Revert "Prevent generating operation without namespace"
cllns Jun 20, 2024
1023225
Add recommendation to add namespace to operations
cllns Jun 20, 2024
6a3c32a
Change examples
cllns Jun 20, 2024
8dc3de4
Switch to outputting directly, remove Files#recommend
cllns Jun 21, 2024
7059e7e
Add Hanami::CLI::RubyFileGenerator
cllns Jun 21, 2024
0dfdf7d
x.x.x => 2.2.0
cllns Jun 22, 2024
8f90b33
x.x.x => 2.2.0
cllns Jun 22, 2024
8e62aa3
Include Dry::Monads[:result] in base Action
cllns Jun 22, 2024
f0e0994
Add .module tests
cllns Jun 24, 2024
ed47e5e
Convert top-level app operation to use RubyFileGenerator
cllns Jun 24, 2024
4769bd6
Convert nested app operation to use RubyFileGenerator
cllns Jun 24, 2024
8e5c575
Support slash separators
cllns Jun 24, 2024
8effa30
Convert top-level slice operation to use RubyFileGenerator
cllns Jun 24, 2024
06f2750
Remove OperationContext
cllns Jun 24, 2024
79699f0
Remove namespaces instance variable
cllns Jun 24, 2024
3879144
Refactor to variables
cllns Jun 24, 2024
6f6fce3
Remove last temporary instance variable
cllns Jun 24, 2024
30c2a94
Refactor
cllns Jun 24, 2024
da093d4
More refactoring, for clarity
cllns Jun 24, 2024
40237de
Rename variable for clarity
cllns Jun 24, 2024
f0cf827
Rename helper method
cllns Jun 24, 2024
2dff130
Simplify RubyFileGenerator, support older versions
cllns Jun 24, 2024
36c7b3b
Convert Operation generator to use simplified RubyFileGenerator
cllns Jun 24, 2024
1c47e7e
Merge branch 'add-dry-operation' into add-file-generator
cllns Jun 24, 2024
2a4f0b6
Remove un-used errors
cllns Jun 24, 2024
0223bea
Refactor
cllns Jun 24, 2024
8e458ce
Older kwargs forwarding style
cllns Jun 24, 2024
d4e13bd
Refactor
cllns Jun 24, 2024
f9221f0
Rename variable
cllns Jun 24, 2024
5f5887b
Add explanatory comment
cllns Jul 1, 2024
e9e7051
Merge remote-tracking branch 'origin/main' into add-file-generator
cllns Jul 5, 2024
a051c7d
Fix base slice action
cllns Jul 5, 2024
d0d02c7
Remove un-used ERB templates
cllns Jul 5, 2024
145f7ee
Remove OperationContext
cllns Jul 5, 2024
b76942c
Ternary over and/or
cllns Jul 5, 2024
26a665b
Fix missing 'end' from bad merge
cllns Jul 5, 2024
b07cf35
Fix namespace recommendation
cllns Jul 5, 2024
23aace2
Merge remote-tracking branch 'origin/main' into add-file-generator
cllns Jul 11, 2024
f649975
Extract App::Generate::Command
cllns Jul 11, 2024
5239824
Specify full name, to use App::Command
cllns Jul 11, 2024
511b6a0
Merge remote-tracking branch 'origin' into add-file-generator
cllns Jul 12, 2024
fffd123
Use constants file
cllns Jul 12, 2024
54dd28b
Move class methods above initialize
cllns Jul 14, 2024
303f502
Use constants file
cllns Jul 14, 2024
e824e5d
Add yard comments
cllns Jul 14, 2024
94c92e1
Revert "Use constants file"
cllns Jul 14, 2024
66d0c80
Fix indent to be two spaces
cllns Jul 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/hanami/cli/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def self.new(
inflector: Dry::Inflector.new,
**opts
)
super(out: out, err: err, fs: fs, inflector: inflector, **opts)
super
end

# Returns a new command.
Expand Down
48 changes: 48 additions & 0 deletions lib/hanami/cli/commands/app/generate/command.rb
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,
Copy link
Member

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 :)

Copy link
Member Author

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 :)

**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
24 changes: 3 additions & 21 deletions lib/hanami/cli/commands/app/generate/operation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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
Expand Down
2 changes: 1 addition & 1 deletion lib/hanami/cli/commands/app/generate/slice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module App
module Generate
# @since 2.0.0
# @api private
class Slice < Command
class Slice < App::Command
argument :name, required: true, desc: "The slice name"
option :url, required: false, type: :string, desc: "The slice URL prefix"

Expand Down
89 changes: 58 additions & 31 deletions lib/hanami/cli/generators/app/operation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "erb"
require "dry/files"
require_relative "../constants"
require_relative "../../errors"

module Hanami
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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 Slice#source_path to make this simpler:

base = slice.source_path

This returns the appropriate value for both the app as well as slices, so it means we no longer need if app else slice conditionals everywhere, which has been a bother for the longest time :)

(Again, this would be another great refactor for post-merge, I don't want to hold anything up!)

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Expand Down
10 changes: 0 additions & 10 deletions lib/hanami/cli/generators/app/operation/nested_app_operation.erb

This file was deleted.

10 changes: 0 additions & 10 deletions lib/hanami/cli/generators/app/operation/nested_slice_operation.erb

This file was deleted.

This file was deleted.

This file was deleted.

71 changes: 0 additions & 71 deletions lib/hanami/cli/generators/app/operation_context.rb

This file was deleted.

Loading