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 all 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
7 changes: 4 additions & 3 deletions lib/hanami/cli/commands/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,12 @@ def self.extended(base)
end

register "generate", aliases: ["g"] do |prefix|
prefix.register "slice", Generate::Slice
prefix.register "action", Generate::Action
prefix.register "view", Generate::View
prefix.register "part", Generate::Part
prefix.register "component", Generate::Component
prefix.register "slice", Generate::Slice
prefix.register "operation", Generate::Operation
prefix.register "part", Generate::Part
prefix.register "view", Generate::View
end
end
end
Expand Down
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 2.2.0
# @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 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)
end
end
end
end
end
end
end
76 changes: 76 additions & 0 deletions lib/hanami/cli/generators/app/operation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# frozen_string_literal: true

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

module Hanami
module CLI
module Generators
module App
# @since 2.2.0
# @api private
class Operation
# @since 2.2.0
# @api private
def initialize(fs:, inflector:, out: $stdout)
@fs = fs
@inflector = inflector
@out = out
end

# @since 2.2.0
# @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, :inflector, :out

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))
out.puts(" Note: We generated a top-level operation. To generate into a directory, add a namespace: `my_namespace.#{context.name}`")
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
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 2.2.0
# @api private
module App
# @since 2.2.0
# @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 2.2.0
# @api private
attr_reader :key

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

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

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

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

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

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

# @since 2.2.0
# @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
6 changes: 6 additions & 0 deletions lib/hanami/cli/generators/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ def bundled_assets?
Hanami.bundled?("hanami-assets")
end

# @since 2.2.0
# @api private
def bundled_dry_monads?
Hanami.bundled?("dry-monads")
end

# @since 2.1.0
# @api private
#
Expand Down
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
3 changes: 3 additions & 0 deletions lib/hanami/cli/generators/gem/app/action.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
# frozen_string_literal: true

require "hanami/action"
require "dry/monads"

module <%= camelized_app_name %>
class Action < Hanami::Action
# Provide `Success` and `Failure` for pattern matching on operation results
include Dry::Monads[:result]
Copy link
Member

@timriley timriley Jun 29, 2024

Choose a reason for hiding this comment

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

Suggested change
include Dry::Monads[:result]
# Provide `Success` and `Failure` for pattern matching on operation results
include Dry::Monads[:result]

I like that you've included this!

I suggest we include a little explainer to make it clear why this line is here.

Copy link
Member

@timriley timriley Jun 29, 2024

Choose a reason for hiding this comment

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

For consistency, can you please update the slice generator to include this in slice actions too?

The difference should be that the slice generator should check to see if dry-monads is bundled before it includes this line. If the user chooses to remove dry-operation from their Gemfile, then dry-monads will disappear and the include would then result in an error.

Copy link
Member Author

Choose a reason for hiding this comment

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

I did this for the slice, but the slice actions inherit from app/action.rb so it doesn't do anything. I kept it in since I might be missing something, but I think we can remove it?

I also had to add a line to require dry/monads

Copy link
Member

Choose a reason for hiding this comment

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

@cllns Ah yeah, sorry, that's my mistake. With slice actions inheriting from an app action, we don't need to put anything additional into the slice actions.

end
end
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