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 dry-operation generators #171

Merged
merged 27 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
27 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
8f90b33
x.x.x => 2.2.0
cllns Jun 22, 2024
8e62aa3
Include Dry::Monads[:result] in base Action
cllns Jun 22, 2024
c2c54c9
Add explanatory comment, and include monads result on Slice action
cllns Jul 1, 2024
9933c8a
Register command so it's available
cllns Jul 1, 2024
9fc5e31
Require dry-monads
cllns Jul 1, 2024
ad0431d
Add require for slice action
cllns Jul 1, 2024
0095497
Change note to past tense
cllns Jul 1, 2024
29b02b9
Merge remote-tracking branch 'origin/main' into add-dry-operation
cllns Jul 5, 2024
e60248d
Remove inlude Dry::Monads in slice Action
cllns Jul 5, 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
50 changes: 50 additions & 0 deletions lib/hanami/cli/commands/app/generate/operation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 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 x.x.x
# @api private
class Operation < App::Command
argument :name, required: true, desc: "Operation name"
option :slice, required: false, desc: "Slice name"

example [
%(books.add (MyApp::Books::Add)),
%(books.add --slice=admin (Admin::Books::Add)),
]
attr_reader :generator
private :generator

# @since x.x.x
# @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 x.x.x
# @api private
def call(name:, slice: nil, **)
slice = inflector.underscore(Shellwords.shellescape(slice)) if slice

generator.call(app.namespace, name, slice)
end
end
end
end
end
end
end
6 changes: 6 additions & 0 deletions lib/hanami/cli/files.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ def chdir(path, &blk)
super
end

# @since x.x.x
# @api private
def recommend(message)
out.puts(" Recommendation: #{message}")
end

private

attr_reader :out
Expand Down
77 changes: 77 additions & 0 deletions lib/hanami/cli/generators/app/operation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

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

module Hanami
module CLI
module Generators
module App
# @since x.x.x
# @api private
class Operation
# @since x.x.x
# @api private
def initialize(fs:, inflector:)
@fs = fs
@inflector = inflector
end

# @since x.x.x
# @api private
def call(app, key, slice)
context = OperationContext.new(inflector, app, slice, key)

if slice
generate_for_slice(context, slice)
else
generate_for_app(context)
end
end

private

attr_reader :fs

attr_reader :inflector

def generate_for_slice(context, slice)
slice_directory = fs.join("slices", slice)
raise MissingSliceError.new(slice) unless fs.directory?(slice_directory)

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))
else
fs.mkdir(directory = fs.join(slice_directory))
fs.write(fs.join(directory, "#{context.name}.rb"), t("top_level_slice_operation.erb", context))
fs.recommend("Add a namespace to operation names, so they go into a folder within #{directory}/.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about this for wording?

Suggested change
fs.recommend("Add a namespace to operation names, so they go into a folder within #{directory}/.")
fs.recommend(%(Generating a top-level operation. To generate into a directory, add a namespace: "my_namespace.#{context.name}))

That first sentence is there to explain why we're offering the recommendation in the first place. The second sentence is there to give a bit more guidance as to what the "recommended" args might look like.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweet, switched it to this. I added the indentation too so it's more noticeable.

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"))
fs.write(fs.join(directory, "#{context.name}.rb"), t("top_level_app_operation.erb", context))
fs.recommend("Add a namespace to operation names, so they go into a folder within #{directory}/.")
end
end

def template(path, context)
require "erb"

ERB.new(
File.read(__dir__ + "/operation/#{path}")
).result(context.ctx)
end

alias_method :t, :template
end
end
end
end
end
10 changes: 10 additions & 0 deletions lib/hanami/cli/generators/app/operation/nested_app_operation.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

module <%= camelized_app_name %>
<%= module_namespace_declaration %>
<%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_app_name %>::Operation
<%= module_namespace_offset %> def call
<%= module_namespace_offset %> end
<%= module_namespace_offset %>end
<%= module_namespace_end %>
end
10 changes: 10 additions & 0 deletions lib/hanami/cli/generators/app/operation/nested_slice_operation.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

module <%= camelized_slice_name %>
<%= module_namespace_declaration %>
<%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_slice_name %>::Operation
<%= module_namespace_offset %> def call
<%= module_namespace_offset %> end
<%= module_namespace_offset %>end
<%= module_namespace_end %>
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

module <%= camelized_app_name %>
<%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_app_name %>::Operation
<%= module_namespace_offset %> def call
<%= module_namespace_offset %> end
<%= module_namespace_offset %>end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

module <%= camelized_slice_name %>
<%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_slice_name %>::Operation
<%= module_namespace_offset %> def call
<%= module_namespace_offset %> end
<%= module_namespace_offset %>end
end
83 changes: 83 additions & 0 deletions lib/hanami/cli/generators/app/operation_context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# frozen_string_literal: true

require_relative "slice_context"
require "dry/files/path"

module Hanami
module CLI
module Generators
# @since x.x.x
# @api private
module App
# @since x.x.x
# @api private
class OperationContext < SliceContext
# TODO: move these constants somewhere that will let us reuse them
KEY_SEPARATOR = %r{\.|/}
private_constant :KEY_SEPARATOR

NAMESPACE_SEPARATOR = "::"
private_constant :NAMESPACE_SEPARATOR

INDENTATION = " "
private_constant :INDENTATION

OFFSET = INDENTATION
private_constant :OFFSET

# @since x.x.x
# @api private
attr_reader :key

# @since x.x.x
# @api private
def initialize(inflector, app, slice, key)
@key = key
super(inflector, app, slice, nil)
end

# @since x.x.x
# @api private
def namespaces
@namespaces ||= key.split(KEY_SEPARATOR)[..-2]
end

# @since x.x.x
# @api private
def name
@name ||= key.split(KEY_SEPARATOR)[-1]
end

# @api private
# @since x.x.x
# @api private
def camelized_name
inflector.camelize(name)
end

# @since x.x.x
# @api private
def module_namespace_declaration
namespaces.each_with_index.map { |token, i|
"#{OFFSET}#{INDENTATION * i}module #{inflector.camelize(token)}"
}.join($/)
end

# @since x.x.x
# @api private
def module_namespace_end
namespaces.each_with_index.map { |_, i|
"#{OFFSET}#{INDENTATION * i}end"
}.reverse.join($/)
end

# @since x.x.x
# @api private
def module_namespace_offset
"#{OFFSET}#{INDENTATION * namespaces.count}"
end
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/hanami/cli/generators/app/slice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def call(app, slice, url, context: SliceContext.new(inflector, app, slice, url))
fs.write(fs.join(directory, "view.rb"), t("view.erb", context))
fs.write(fs.join(directory, "views", "helpers.rb"), t("helpers.erb", context))
fs.write(fs.join(directory, "templates", "layouts", "app.html.erb"), t("app_layout.erb", context))
fs.write(fs.join(directory, "operation.rb"), t("operation.erb", context))

if context.bundled_assets?
fs.write(fs.join(directory, "assets", "js", "app.js"), t("app_js.erb", context))
Expand Down
7 changes: 7 additions & 0 deletions lib/hanami/cli/generators/app/slice/operation.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# auto_register: false
# frozen_string_literal: true

module <%= camelized_slice_name %>
class Operation < <%= camelized_app_name %>::Operation
end
end
2 changes: 2 additions & 0 deletions lib/hanami/cli/generators/gem/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ def generate_app(app, context) # rubocop:disable Metrics/AbcSize
fs.write("app/assets/images/favicon.ico", file("favicon.ico"))
end

fs.write("app/operation.rb", t("operation.erb", context))

fs.write("public/404.html", file("404.html"))
fs.write("public/500.html", file("500.html"))
end
Expand Down
1 change: 1 addition & 0 deletions lib/hanami/cli/generators/gem/app/gemfile.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ source "https://rubygems.org"
<%= hanami_gem("view") %>

gem "dry-types", "~> 1.0", ">= 1.6.1"
gem "dry-operation", github: "dry-rb/dry-operation"
gem "puma"
gem "rake"

Expand Down
9 changes: 9 additions & 0 deletions lib/hanami/cli/generators/gem/app/operation.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# auto_register: false
# frozen_string_literal: true

require "dry/operation"

module <%= camelized_app_name %>
class Operation < Dry::Operation
end
end
Loading