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

Convert Action generator to use RubyClassFile #288

Open
wants to merge 53 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
ebfb990
Start conversion of View generator to use RubyFileWriter
cllns Jan 3, 2025
3b727b8
Finish conversion, though needs to be cleaned up
cllns Jan 3, 2025
81d0a8d
Extract RubyClassFile to its own file
cllns Jan 3, 2025
eb7bf14
Refactor
cllns Jan 3, 2025
2e019c7
More refactoring
cllns Jan 3, 2025
9f58101
Remove un-used files
cllns Jan 4, 2025
a93cfaf
Clean up
cllns Jan 4, 2025
58e5850
Extract #key_parts private method
cllns Jan 4, 2025
33efdef
Fix @since versions
cllns Feb 7, 2025
e09ff34
Convert generating Action file in app to use RubyClassFile
cllns Feb 17, 2025
d253e02
Use view generator in action, instead of re-implementing behavior
cllns Feb 17, 2025
488fa49
Add specs for preventing overwriting view class and template files
cllns Feb 17, 2025
bb115b2
Refactor for clarity
cllns Feb 17, 2025
abe584e
Convert generating Action file in slice to use RubyClassFile
cllns Feb 17, 2025
9c7bca1
Use view generator in slice action, too
cllns Feb 17, 2025
09a9763
Make generate method signatures identical
cllns Feb 17, 2025
646e813
Remove ERB templates and code to render them
cllns Feb 17, 2025
dd0b320
Dry up key creation
cllns Feb 17, 2025
cbc988a
Extract bundled_views, in preparation for removing ActionContext
cllns Feb 17, 2025
8f9a09f
Use namespace instead of context
cllns Feb 17, 2025
2a93e21
Remove use of context
cllns Feb 18, 2025
9492b92
Add base_path
cllns Feb 18, 2025
ff50277
Simplify logic with unless
cllns Feb 18, 2025
7e9785e
Move route logic to conditional
cllns Feb 18, 2025
bfe1e59
Finally combine methods into one
cllns Feb 18, 2025
33b9093
Remove url from generate_files parameters
cllns Feb 18, 2025
63a5b01
Split into controller and action within generator
cllns Feb 18, 2025
2b83a00
Remove unused parameters, move slice check to command
cllns Feb 18, 2025
5068e6a
Simplify branches
cllns Feb 18, 2025
a2009e6
Separate generate methods
cllns Feb 18, 2025
c4316b9
Store view generator as instance variable
cllns Feb 18, 2025
c1a94ce
Extract insert_route helper method
cllns Feb 18, 2025
56b20e3
Standardize on kwargs
cllns Feb 18, 2025
55bc101
Convert insert_route to use key directly
cllns Feb 18, 2025
37e6749
Move skip_view check outside of generate_view
cllns Feb 18, 2025
0e6e87d
Rename to http_verb for clarity
cllns Feb 18, 2025
a21c05e
Remove passing in 'slice', instead use the namespace
cllns Feb 18, 2025
eb6289c
More kwargs
cllns Feb 18, 2025
95741fa
Use initializer aliases for more descriptive names
cllns Feb 18, 2025
cbc0b01
Move skip_route out of insert_route
cllns Feb 18, 2025
a9f0a7d
Refactor insert_route
cllns Feb 18, 2025
cafd1d4
Tidy up
cllns Feb 18, 2025
3caf5ee
Rename to http_method, add whitespace around params and options
cllns Feb 18, 2025
13aee91
Convert Generate Action to use base Generate Command
cllns Feb 18, 2025
214e42d
Remove unused kwarg
cllns Feb 18, 2025
9430ca0
Reorder kwargs
cllns Feb 18, 2025
801bc8c
Remove un-used parameter
cllns Feb 18, 2025
1d1d91e
Remove another un-used parameter
cllns Feb 18, 2025
c94d72d
Rubocop fixes
cllns Feb 18, 2025
dfbf979
No bare double splats (only in 3.2+ and we need 3.1+ for now)
cllns Feb 18, 2025
a82edbd
Reorder methods in order of use
cllns Feb 18, 2025
6479a33
Merge remote-tracking branch 'origin/main' into expand-use-of-ruby-fi…
cllns Feb 27, 2025
a233e12
Add version
cllns Feb 27, 2025
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
72 changes: 32 additions & 40 deletions lib/hanami/cli/commands/app/generate/action.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module App
module Generate
# @since 2.0.0
# @api private
class Action < App::Command
class Action < Command
# TODO: ideally the default format should lookup
# slice configuration (Action's `default_response_format`)
DEFAULT_FORMAT = "html"
Expand All @@ -29,29 +29,37 @@ class Action < App::Command
private_constant :DEFAULT_SKIP_ROUTE

argument :name, required: true, desc: "Action name"
option :url, required: false, type: :string, desc: "Action URL"
option :http, required: false, type: :string, desc: "Action HTTP method"
# option :format, required: false, type: :string, default: DEFAULT_FORMAT, desc: "Template format"

option :url, as: :url_path, required: false, type: :string, desc: "Action URL path"

option :http, as: :http_method, required: false, type: :string, desc: "Action HTTP method"

option \
:skip_view,
required: false,
type: :flag,
default: DEFAULT_SKIP_VIEW,
desc: "Skip view and template generation"

# TODO: Implement this
option \
:skip_tests,
required: false,
type: :flag,
default: DEFAULT_SKIP_TESTS,
desc: "Skip test generation"

option \
:skip_route,
required: false,
type: :flag,
default: DEFAULT_SKIP_ROUTE,
desc: "Skip route generation"

option :slice, required: false, desc: "Slice name"

# option :format, required: false, type: :string, default: DEFAULT_FORMAT, desc: "Template format"

# rubocop:disable Layout/LineLength
example [
%(books.index # GET /books to: "books.index" (MyApp::Actions::Books::Index)),
Expand All @@ -68,52 +76,36 @@ class Action < App::Command
]
# rubocop:enable Layout/LineLength

# @since 2.0.0
# @api private
def initialize(
fs:, inflector:,
naming: Naming.new(inflector: inflector),
generator: Generators::App::Action.new(fs: fs, inflector: inflector),
**opts
)
super(fs: fs, inflector: inflector, **opts)

@naming = naming
@generator = generator
def generator_class
Generators::App::Action
end

# rubocop:disable Metrics/ParameterLists

# @since 2.0.0
# @api private
# rubocop:disable Lint/ParameterLists
def call(
name:,
url: nil,
http: nil,
format: DEFAULT_FORMAT,
slice: nil,
url_path: nil,
http_method: nil,
skip_view: DEFAULT_SKIP_VIEW,
skip_tests: DEFAULT_SKIP_TESTS, # rubocop:disable Lint/UnusedMethodArgument,
skip_route: DEFAULT_SKIP_ROUTE,
slice: nil,
context: nil,
**
skip_tests: DEFAULT_SKIP_TESTS # rubocop:disable Lint/UnusedMethodArgument
)
slice = inflector.underscore(Shellwords.shellescape(slice)) if slice
name = naming.action_name(name)
*controller, action = name.split(ACTION_SEPARATOR)

if controller.empty?
raise InvalidActionNameError.new(name)
end

generator.call(app.namespace, controller, action, url, http, format, skip_view, skip_route, slice, context: context)
name = Naming.new(inflector:).action_name(name)

raise InvalidActionNameError.new(name) unless name.include?(ACTION_SEPARATOR)

super(
name:,
slice:,
url_path:,
skip_route:,
http_method:,
skip_view: skip_view || !Hanami.bundled?("hanami-view"),
)
end

# rubocop:enable Metrics/ParameterLists

private

attr_reader :naming, :generator
# rubocop:enable Lint/ParameterLists
end
end
end
Expand Down
11 changes: 8 additions & 3 deletions lib/hanami/cli/commands/app/generate/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ class Command < App::Command
attr_reader :generator
private :generator

attr_reader :inflector
private :inflector

# @since 2.2.0
# @api private
def initialize(
fs:,
inflector:,
**opts
**
)
super
@generator = generator_class.new(fs: fs, inflector: inflector, out: out)
Expand All @@ -37,7 +40,7 @@ def generator_class

# @since 2.2.0
# @api private
def call(name:, slice: nil, **)
def call(name:, slice: nil, **opts)
if slice
base_path = fs.join("slices", inflector.underscore(slice))
raise MissingSliceError.new(slice) unless fs.exist?(base_path)
Expand All @@ -46,12 +49,14 @@ def call(name:, slice: nil, **)
key: name,
namespace: slice,
base_path: base_path,
**opts,
)
else
generator.call(
key: name,
namespace: app.namespace,
base_path: "app"
base_path: "app",
**opts,
)
end
end
Expand Down
179 changes: 78 additions & 101 deletions lib/hanami/cli/generators/app/action.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

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

# rubocop:disable Metrics/ParameterLists
Expand All @@ -14,20 +14,25 @@ module App
class Action
# @since 2.0.0
# @api private
def initialize(fs:, inflector:)
def initialize(fs:, inflector:, out: $stdout)
@fs = fs
@inflector = inflector
@out = out
@view_generator = Generators::App::View.new(
fs: fs,
inflector: inflector,
out: out
)
end

# @since 2.0.0
# @api private
def call(app, controller, action, url, http, format, skip_view, skip_route, slice, context: nil)
context ||= ActionContext.new(inflector, app, slice, controller, action)
if slice
generate_for_slice(controller, action, url, http, format, skip_view, skip_route, slice, context)
else
generate_for_app(controller, action, url, http, format, skip_view, skip_route, context)
end
def call(key:, namespace:, base_path:, url_path:, http_method:, skip_view:, skip_route:)
insert_route(key:, namespace:, url_path:, http_method:) unless skip_route

generate_action(key:, namespace:, base_path:, include_placeholder_body: skip_view)

generate_view(key:, namespace:, base_path:) unless skip_view
end

private
Expand Down Expand Up @@ -67,86 +72,76 @@ def call(app, controller, action, url, http, format, skip_view, skip_route, slic
PATH_SEPARATOR = "/"
private_constant :PATH_SEPARATOR

attr_reader :fs

attr_reader :inflector

# rubocop:disable Metrics/AbcSize
def generate_for_slice(controller, action, url, http, format, skip_view, skip_route, slice, context)
slice_directory = fs.join("slices", slice)
raise MissingSliceError.new(slice) unless fs.directory?(slice_directory)

if generate_route?(skip_route)
fs.inject_line_at_block_bottom(
fs.join("config", "routes.rb"),
slice_matcher(slice),
route(controller, action, url, http)
)
end

fs.mkdir(directory = fs.join(slice_directory, "actions", controller))
fs.create(fs.join(directory, "#{action}.rb"), t("slice_action.erb", context))
attr_reader :fs, :inflector, :out, :view_generator

if generate_view?(skip_view, action, directory)
fs.mkdir(directory = fs.join(slice_directory, "views", controller))
fs.create(fs.join(directory, "#{action}.rb"), t("slice_view.erb", context))
# @api private
# @since 2.2.2
def insert_route(key:, namespace:, url_path:, http_method:)
routes_location = fs.join("config", "routes.rb")
route = route_definition(key:, url_path:, http_method:)

fs.mkdir(directory = fs.join(slice_directory, "templates", controller))
fs.create(fs.join(directory, "#{action}.#{format}.erb"),
t(template_with_format_ext("slice_template", format), context))
if namespace == Hanami.app.namespace
fs.inject_line_at_class_bottom(routes_location, "class Routes", route)
else
slice_matcher = /slice[[:space:]]*:#{namespace}/
fs.inject_line_at_block_bottom(routes_location, slice_matcher, route)
end
end

def generate_for_app(controller, action, url, http, format, skip_view, skip_route, context)
if generate_route?(skip_route)
fs.inject_line_at_class_bottom(
fs.join("config", "routes.rb"),
"class Routes",
route(controller, action, url, http)
)
end

fs.mkdir(directory = fs.join("app", "actions", controller))
fs.create(fs.join(directory, "#{action}.rb"), t("action.erb", context))
# @api private
# @since 2.2.2
def generate_action(key:, namespace:, base_path:, include_placeholder_body:)
RubyClassFile.new(
fs: fs,
inflector: inflector,
namespace: namespace,
key: inflector.underscore(key),
base_path: base_path,
relative_parent_class: "Action",
extra_namespace: "Actions",
body: [
"def handle(request, response)",
(" response.body = self.class.name" if include_placeholder_body),
"end"
].compact
).create
end

view = action
view_directory = fs.join("app", "views", controller)
# @api private
# @since 2.2.2
def generate_view(key:, namespace:, base_path:)
*controller_name_parts, action_name = key.split(KEY_SEPARATOR)

if generate_view?(skip_view, view, view_directory)
fs.mkdir(view_directory)
fs.create(fs.join(view_directory, "#{view}.rb"), t("view.erb", context))
view_directory = fs.join(base_path, "views", controller_name_parts)

fs.mkdir(template_directory = fs.join("app", "templates", controller))
fs.create(fs.join(template_directory, "#{view}.#{format}.erb"),
t(template_with_format_ext("template", format), context))
if generate_view?(action_name, view_directory)
view_generator.call(
key: key,
namespace: namespace,
base_path: base_path,
)
end
end
# rubocop:enable Metrics/AbcSize

def slice_matcher(slice)
/slice[[:space:]]*:#{slice}/
end

def route(controller, action, url, http)
%(#{route_http(action,
http)} "#{route_url(controller, action, url)}", to: "#{controller.join('.')}.#{action}")
end

# @api private
# @since 2.1.0
def generate_view?(skip_view, view, directory)
return false if skip_view
return generate_restful_view?(view, directory) if rest_view?(view)
# @since 2.2.2
def route_definition(key:, url_path:, http_method:)
*controller_name_parts, action_name = key.split(KEY_SEPARATOR)

method = route_http(action_name, http_method)
path = route_url(controller_name_parts, action_name, url_path)

true
%(#{method} "#{path}", to: "#{key}")
end

# @api private
# @since 2.2.0
def generate_route?(skip_route)
return false if skip_route

true
# @since 2.1.0
def generate_view?(action_name, directory)
if rest_view?(action_name)
generate_restful_view?(action_name, directory)
else
true
end
end

# @api private
Expand All @@ -169,40 +164,22 @@ def corresponding_restful_view(view)
RESTFUL_COUNTERPART_VIEWS.fetch(view, nil)
end

def template_with_format_ext(name, format)
ext =
case format.to_sym
when :html
".html.erb"
else
".erb"
end

"#{name}#{ext}"
end

def template(path, context)
require "erb"

ERB.new(
File.read(__dir__ + "/action/#{path}"), trim_mode: "-",
).result(context.ctx)
end

alias_method :t, :template

def route_url(controller, action, url)
# @api private
# @since 2.1.0
def route_url(controller, action, url_path)
action = ROUTE_RESTFUL_URL_SUFFIXES.fetch(action) { [action] }
url ||= "#{PATH_SEPARATOR}#{(controller + action).join(PATH_SEPARATOR)}"
url_path ||= "#{PATH_SEPARATOR}#{(controller + action).join(PATH_SEPARATOR)}"

CLI::URL.call(url)
CLI::URL.call(url_path)
end

def route_http(action, http)
result = (http ||= ROUTE_RESTFUL_HTTP_METHODS.fetch(action, ROUTE_DEFAULT_HTTP_METHOD)).downcase
# @api private
# @since 2.1.0
def route_http(action, http_method)
result = (http_method ||= ROUTE_RESTFUL_HTTP_METHODS.fetch(action, ROUTE_DEFAULT_HTTP_METHOD)).downcase

unless ROUTE_HTTP_METHODS.include?(result)
raise UnknownHTTPMethodError.new(http)
raise UnknownHTTPMethodError.new(http_method)
end

result
Expand Down
Loading