Skip to content

Commit 2ffa120

Browse files
authored
Add dry-operation generators (#171)
* 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 * x.x.x => 2.2.0 * Include Dry::Monads[:result] in base Action * Add explanatory comment, and include monads result on Slice action Add dry-monads include for slice base action * Register command so it's available * Require dry-monads * Add require for slice action * Change note to past tense * Remove inlude Dry::Monads in slice Action
1 parent 102f4ca commit 2ffa120

19 files changed

+472
-6
lines changed

lib/hanami/cli/commands/app.rb

+4-3
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,12 @@ def self.extended(base)
4141
end
4242

4343
register "generate", aliases: ["g"] do |prefix|
44-
prefix.register "slice", Generate::Slice
4544
prefix.register "action", Generate::Action
46-
prefix.register "view", Generate::View
47-
prefix.register "part", Generate::Part
4845
prefix.register "component", Generate::Component
46+
prefix.register "slice", Generate::Slice
47+
prefix.register "operation", Generate::Operation
48+
prefix.register "part", Generate::Part
49+
prefix.register "view", Generate::View
4950
end
5051
end
5152
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 Operation < App::Command
17+
argument :name, required: true, desc: "Operation name"
18+
option :slice, required: false, desc: "Slice name"
19+
20+
example [
21+
%(books.add (MyApp::Books::Add)),
22+
%(books.add --slice=admin (Admin::Books::Add)),
23+
]
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
37+
38+
# @since 2.2.0
39+
# @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)
44+
end
45+
end
46+
end
47+
end
48+
end
49+
end
50+
end
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# frozen_string_literal: true
2+
3+
require "erb"
4+
require "dry/files"
5+
require_relative "../../errors"
6+
7+
module Hanami
8+
module CLI
9+
module Generators
10+
module App
11+
# @since 2.2.0
12+
# @api private
13+
class Operation
14+
# @since 2.2.0
15+
# @api private
16+
def initialize(fs:, inflector:, out: $stdout)
17+
@fs = fs
18+
@inflector = inflector
19+
@out = out
20+
end
21+
22+
# @since 2.2.0
23+
# @api private
24+
def call(app, key, slice)
25+
context = OperationContext.new(inflector, app, slice, key)
26+
27+
if slice
28+
generate_for_slice(context, slice)
29+
else
30+
generate_for_app(context)
31+
end
32+
end
33+
34+
private
35+
36+
attr_reader :fs, :inflector, :out
37+
38+
def generate_for_slice(context, slice)
39+
slice_directory = fs.join("slices", slice)
40+
raise MissingSliceError.new(slice) unless fs.directory?(slice_directory)
41+
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))
45+
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}`")
49+
end
50+
end
51+
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
61+
end
62+
63+
def template(path, context)
64+
require "erb"
65+
66+
ERB.new(
67+
File.read(__dir__ + "/operation/#{path}")
68+
).result(context.ctx)
69+
end
70+
71+
alias_method :t, :template
72+
end
73+
end
74+
end
75+
end
76+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# frozen_string_literal: true
2+
3+
module <%= camelized_app_name %>
4+
<%= module_namespace_declaration %>
5+
<%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_app_name %>::Operation
6+
<%= module_namespace_offset %> def call
7+
<%= module_namespace_offset %> end
8+
<%= module_namespace_offset %>end
9+
<%= module_namespace_end %>
10+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# frozen_string_literal: true
2+
3+
module <%= camelized_slice_name %>
4+
<%= module_namespace_declaration %>
5+
<%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_slice_name %>::Operation
6+
<%= module_namespace_offset %> def call
7+
<%= module_namespace_offset %> end
8+
<%= module_namespace_offset %>end
9+
<%= module_namespace_end %>
10+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# frozen_string_literal: true
2+
3+
module <%= camelized_app_name %>
4+
<%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_app_name %>::Operation
5+
<%= module_namespace_offset %> def call
6+
<%= module_namespace_offset %> end
7+
<%= module_namespace_offset %>end
8+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# frozen_string_literal: true
2+
3+
module <%= camelized_slice_name %>
4+
<%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_slice_name %>::Operation
5+
<%= module_namespace_offset %> def call
6+
<%= module_namespace_offset %> end
7+
<%= module_namespace_offset %>end
8+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "slice_context"
4+
require "dry/files/path"
5+
6+
module Hanami
7+
module CLI
8+
module Generators
9+
# @since 2.2.0
10+
# @api private
11+
module App
12+
# @since 2.2.0
13+
# @api private
14+
class OperationContext < SliceContext
15+
# TODO: move these constants somewhere that will let us reuse them
16+
KEY_SEPARATOR = %r{\.|/}
17+
private_constant :KEY_SEPARATOR
18+
19+
NAMESPACE_SEPARATOR = "::"
20+
private_constant :NAMESPACE_SEPARATOR
21+
22+
INDENTATION = " "
23+
private_constant :INDENTATION
24+
25+
OFFSET = INDENTATION
26+
private_constant :OFFSET
27+
28+
# @since 2.2.0
29+
# @api private
30+
attr_reader :key
31+
32+
# @since 2.2.0
33+
# @api private
34+
def initialize(inflector, app, slice, key)
35+
@key = key
36+
super(inflector, app, slice, nil)
37+
end
38+
39+
# @since 2.2.0
40+
# @api private
41+
def namespaces
42+
@namespaces ||= key.split(KEY_SEPARATOR)[..-2]
43+
end
44+
45+
# @since 2.2.0
46+
# @api private
47+
def name
48+
@name ||= key.split(KEY_SEPARATOR)[-1]
49+
end
50+
51+
# @api private
52+
# @since 2.2.0
53+
# @api private
54+
def camelized_name
55+
inflector.camelize(name)
56+
end
57+
58+
# @since 2.2.0
59+
# @api private
60+
def module_namespace_declaration
61+
namespaces.each_with_index.map { |token, i|
62+
"#{OFFSET}#{INDENTATION * i}module #{inflector.camelize(token)}"
63+
}.join($/)
64+
end
65+
66+
# @since 2.2.0
67+
# @api private
68+
def module_namespace_end
69+
namespaces.each_with_index.map { |_, i|
70+
"#{OFFSET}#{INDENTATION * i}end"
71+
}.reverse.join($/)
72+
end
73+
74+
# @since 2.2.0
75+
# @api private
76+
def module_namespace_offset
77+
"#{OFFSET}#{INDENTATION * namespaces.count}"
78+
end
79+
end
80+
end
81+
end
82+
end
83+
end

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

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def call(app, slice, url, context: SliceContext.new(inflector, app, slice, url))
3131
fs.write(fs.join(directory, "view.rb"), t("view.erb", context))
3232
fs.write(fs.join(directory, "views", "helpers.rb"), t("helpers.erb", context))
3333
fs.write(fs.join(directory, "templates", "layouts", "app.html.erb"), t("app_layout.erb", context))
34+
fs.write(fs.join(directory, "operation.rb"), t("operation.erb", context))
3435

3536
if context.bundled_assets?
3637
fs.write(fs.join(directory, "assets", "js", "app.js"), t("app_js.erb", context))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# auto_register: false
2+
# frozen_string_literal: true
3+
4+
module <%= camelized_slice_name %>
5+
class Operation < <%= camelized_app_name %>::Operation
6+
end
7+
end

lib/hanami/cli/generators/context.rb

+6
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ def bundled_assets?
9292
Hanami.bundled?("hanami-assets")
9393
end
9494

95+
# @since 2.2.0
96+
# @api private
97+
def bundled_dry_monads?
98+
Hanami.bundled?("dry-monads")
99+
end
100+
95101
# @since 2.1.0
96102
# @api private
97103
#

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

+2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ def generate_app(app, context) # rubocop:disable Metrics/AbcSize
6868
fs.write("app/assets/images/favicon.ico", file("favicon.ico"))
6969
end
7070

71+
fs.write("app/operation.rb", t("operation.erb", context))
72+
7173
fs.write("public/404.html", file("404.html"))
7274
fs.write("public/500.html", file("500.html"))
7375
end

lib/hanami/cli/generators/gem/app/action.erb

+3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
# frozen_string_literal: true
33

44
require "hanami/action"
5+
require "dry/monads"
56

67
module <%= camelized_app_name %>
78
class Action < Hanami::Action
9+
# Provide `Success` and `Failure` for pattern matching on operation results
10+
include Dry::Monads[:result]
811
end
912
end

lib/hanami/cli/generators/gem/app/gemfile.erb

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ source "https://rubygems.org"
1212
<%= hanami_gem("view") %>
1313

1414
gem "dry-types", "~> 1.0", ">= 1.6.1"
15+
gem "dry-operation", github: "dry-rb/dry-operation"
1516
gem "puma"
1617
gem "rake"
1718

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# auto_register: false
2+
# frozen_string_literal: true
3+
4+
require "dry/operation"
5+
6+
module <%= camelized_app_name %>
7+
class Operation < Dry::Operation
8+
end
9+
end

0 commit comments

Comments
 (0)