Skip to content

Commit cb06023

Browse files
authored
Generate struct (with RubyFileGenerator, add RubyFileWriter) (#199)
* Add struct generator * Extract App::Generate::Command * Specify full name, to use App::Command * Use App::Generate::Command for Struct * Use KEY_SEPARATOR from constants file * Provide generator class to command * Extract Helper, use for Struct generator * Fix specs with Helper * kwargs * Rename helper to RubyFileWriter * Fix examples * Remove optional kwarg * Reorder args * Rename to relative_parent_class * Remove duplicate implementation * Add api doc comments * Reorder methods * Refactor initialize * Refactor to use method instead of arg * Refactor to move logic into generator * Reorder assignments * Add Hanami::CLI::RubyFileGenerator, convert Operation to use it instead of ERB (#186) * 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 * Add Hanami::CLI::RubyFileGenerator * x.x.x => 2.2.0 * x.x.x => 2.2.0 * Include Dry::Monads[:result] in base Action * Add .module tests * Convert top-level app operation to use RubyFileGenerator * Convert nested app operation to use RubyFileGenerator * Support slash separators * Convert top-level slice operation to use RubyFileGenerator * Remove OperationContext * Remove namespaces instance variable * Refactor to variables * Remove last temporary instance variable * Refactor * More refactoring, for clarity * Rename variable for clarity * Rename helper method * Simplify RubyFileGenerator, support older versions * Convert Operation generator to use simplified RubyFileGenerator * Remove un-used errors * Refactor * Older kwargs forwarding style * Refactor * Rename variable * Add explanatory comment Add dry-monads include for slice base action * Fix base slice action * Remove un-used ERB templates * Remove OperationContext * Ternary over and/or * Fix missing 'end' from bad merge * Fix namespace recommendation * Extract App::Generate::Command * Specify full name, to use App::Command * Use constants file * Move class methods above initialize * Use constants file * Add yard comments * Revert "Use constants file" This reverts commit 303f502. Would need to namespace it and we may want to this to standalone so keeping it here. It's just two little spaces anyway * Fix indent to be two spaces * Remove extraneous requires * Use out.string.chomp * Fix name of expectation
1 parent 66d0c80 commit cb06023

File tree

9 files changed

+364
-84
lines changed

9 files changed

+364
-84
lines changed

lib/hanami/cli/commands/app.rb

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def self.extended(base)
4646
prefix.register "operation", Generate::Operation
4747
prefix.register "part", Generate::Part
4848
prefix.register "slice", Generate::Slice
49+
prefix.register "struct", Generate::Struct
4950
prefix.register "view", Generate::View
5051
end
5152
end

lib/hanami/cli/commands/app/generate/command.rb

+6-4
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,17 @@ class Command < App::Command
2525
def initialize(
2626
fs:,
2727
inflector:,
28-
generator_class: nil,
2928
**opts
3029
)
31-
raise "Provide a generator class (that takes fs and inflector)" if generator_class.nil?
32-
33-
super(fs: fs, inflector: inflector, **opts)
30+
super
3431
@generator = generator_class.new(fs: fs, inflector: inflector, out: out)
3532
end
3633

34+
def generator_class
35+
# Must be implemented by subclasses, with class that takes:
36+
# fs:, inflector:, out:
37+
end
38+
3739
# @since 2.2.0
3840
# @api private
3941
def call(name:, slice: nil, **)

lib/hanami/cli/commands/app/generate/operation.rb

+2-10
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
# frozen_string_literal: true
22

3-
require "dry/inflector"
4-
require "dry/files"
5-
require "shellwords"
6-
require_relative "../../../naming"
7-
require_relative "../../../errors"
8-
93
module Hanami
104
module CLI
115
module Commands
@@ -19,10 +13,8 @@ class Operation < Generate::Command
1913
%(books.add --slice=admin (Admin::Books::Add)),
2014
]
2115

22-
# @since 2.2.0
23-
# @api private
24-
def initialize(**opts)
25-
super(generator_class: Generators::App::Operation, **opts)
16+
def generator_class
17+
Generators::App::Operation
2618
end
2719
end
2820
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# frozen_string_literal: true
2+
3+
module Hanami
4+
module CLI
5+
module Commands
6+
module App
7+
module Generate
8+
# @since 2.2.0
9+
# @api private
10+
class Struct < Command
11+
argument :name, required: true, desc: "Struct name"
12+
option :slice, required: false, desc: "Slice name"
13+
14+
example [
15+
%(book (MyApp::Structs::Book)),
16+
%(book/published_book (MyApp::Structs::Book::PublishedBook)),
17+
%(book --slice=admin (Admin::Structs::Book)),
18+
]
19+
20+
def generator_class
21+
Generators::App::Struct
22+
end
23+
end
24+
end
25+
end
26+
end
27+
end
28+
end

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

+15-67
Original file line numberDiff line numberDiff line change
@@ -23,79 +23,27 @@ def initialize(fs:, inflector:, out: $stdout)
2323
# @since 2.2.0
2424
# @api private
2525
def call(app_namespace, key, slice)
26-
operation_name = key.split(KEY_SEPARATOR)[-1]
27-
local_namespaces = key.split(KEY_SEPARATOR)[..-2]
28-
container_namespace = slice || app_namespace
29-
30-
raise_missing_slice_error_if_missing(slice) if slice
31-
print_namespace_recommendation(operation_name) if local_namespaces.none?
32-
33-
directory = directory(slice, local_namespaces: local_namespaces)
34-
path = fs.join(directory, "#{operation_name}.rb")
35-
fs.mkdir(directory)
26+
RubyFileWriter.new(
27+
fs: fs,
28+
inflector: inflector,
29+
app_namespace: app_namespace,
30+
key: key,
31+
slice: slice,
32+
relative_parent_class: "Operation",
33+
body: ["def call", "end"],
34+
).call
3635

37-
file_contents = class_definition(
38-
operation_name: operation_name,
39-
container_namespace: container_namespace,
40-
local_namespaces: local_namespaces,
41-
)
42-
fs.write(path, file_contents)
36+
unless key.match?(KEY_SEPARATOR)
37+
out.puts(
38+
" Note: We generated a top-level operation. " \
39+
"To generate into a directory, add a namespace: `my_namespace.add_book`"
40+
)
41+
end
4342
end
4443

4544
private
4645

4746
attr_reader :fs, :inflector, :out
48-
49-
def directory(slice = nil, local_namespaces:)
50-
base = if slice
51-
fs.join("slices", slice)
52-
else
53-
fs.join("app")
54-
end
55-
56-
if local_namespaces.any?
57-
fs.join(base, local_namespaces)
58-
else
59-
fs.join(base)
60-
end
61-
end
62-
63-
def class_definition(operation_name:, container_namespace:, local_namespaces:)
64-
container_module = normalize(container_namespace)
65-
66-
modules = local_namespaces
67-
.map { normalize(_1) }
68-
.compact
69-
.prepend(container_module)
70-
71-
parent_class = [container_module, "Operation"].join("::")
72-
73-
RubyFileGenerator.class(
74-
normalize(operation_name),
75-
parent_class: parent_class,
76-
modules: modules,
77-
body: ["def call", "end"],
78-
header: ["# frozen_string_literal: true"],
79-
)
80-
end
81-
82-
def normalize(name)
83-
inflector.camelize(name).gsub(/[^\p{Alnum}]/, "")
84-
end
85-
86-
def print_namespace_recommendation(operation_name)
87-
out.puts(
88-
" Note: We generated a top-level operation. " \
89-
"To generate into a directory, add a namespace: `my_namespace.#{operation_name}`"
90-
)
91-
end
92-
93-
def raise_missing_slice_error_if_missing(slice)
94-
if slice
95-
slice_directory = fs.join("slices", slice)
96-
raise MissingSliceError.new(slice) unless fs.directory?(slice_directory)
97-
end
98-
end
9947
end
10048
end
10149
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# frozen_string_literal: true
2+
3+
require "erb"
4+
require "dry/files"
5+
require_relative "../constants"
6+
require_relative "../../errors"
7+
8+
module Hanami
9+
module CLI
10+
module Generators
11+
module App
12+
# @since 2.2.0
13+
# @api private
14+
class RubyFileWriter
15+
# @since 2.2.0
16+
# @api private
17+
def initialize(
18+
fs:,
19+
inflector:,
20+
app_namespace:,
21+
key:,
22+
slice:,
23+
relative_parent_class:,
24+
extra_namespace: nil,
25+
body: []
26+
)
27+
@fs = fs
28+
@inflector = inflector
29+
@app_namespace = app_namespace
30+
@key = key
31+
@slice = slice
32+
@extra_namespace = extra_namespace&.downcase
33+
@relative_parent_class = relative_parent_class
34+
@body = body
35+
raise_missing_slice_error_if_missing(slice) if slice
36+
end
37+
38+
# @since 2.2.0
39+
# @api private
40+
def call
41+
fs.mkdir(directory)
42+
fs.write(path, file_contents)
43+
end
44+
45+
private
46+
47+
# @since 2.2.0
48+
# @api private
49+
attr_reader(
50+
:fs,
51+
:inflector,
52+
:app_namespace,
53+
:key,
54+
:slice,
55+
:extra_namespace,
56+
:relative_parent_class,
57+
:body,
58+
)
59+
60+
# @since 2.2.0
61+
# @api private
62+
def file_contents
63+
class_definition(
64+
class_name: class_name,
65+
container_namespace: container_namespace,
66+
local_namespaces: local_namespaces,
67+
)
68+
end
69+
70+
# @since 2.2.0
71+
# @api private
72+
def class_name
73+
key.split(KEY_SEPARATOR)[-1]
74+
end
75+
76+
# @since 2.2.0
77+
# @api private
78+
def container_namespace
79+
slice || app_namespace
80+
end
81+
82+
# @since 2.2.0
83+
# @api private
84+
def local_namespaces
85+
Array(extra_namespace) + key.split(KEY_SEPARATOR)[..-2]
86+
end
87+
88+
# @since 2.2.0
89+
# @api private
90+
def directory
91+
base = if slice
92+
fs.join("slices", slice)
93+
else
94+
fs.join("app")
95+
end
96+
97+
@directory ||= if local_namespaces.any?
98+
fs.join(base, local_namespaces)
99+
else
100+
fs.join(base)
101+
end
102+
end
103+
104+
# @since 2.2.0
105+
# @api private
106+
def path
107+
fs.join(directory, "#{class_name}.rb")
108+
end
109+
110+
# @since 2.2.0
111+
# @api private
112+
def class_definition(class_name:, container_namespace:, local_namespaces:)
113+
container_module = normalize(container_namespace)
114+
115+
modules = local_namespaces
116+
.map { normalize(_1) }
117+
.compact
118+
.prepend(container_module)
119+
120+
parent_class = [container_module, relative_parent_class].join("::")
121+
122+
RubyFileGenerator.class(
123+
normalize(class_name),
124+
parent_class: parent_class,
125+
modules: modules,
126+
header: ["# frozen_string_literal: true"],
127+
body: body
128+
)
129+
end
130+
131+
# @since 2.2.0
132+
# @api private
133+
def normalize(name)
134+
inflector.camelize(name).gsub(/[^\p{Alnum}]/, "")
135+
end
136+
137+
# @since 2.2.0
138+
# @api private
139+
def raise_missing_slice_error_if_missing(slice)
140+
if slice
141+
slice_directory = fs.join("slices", slice)
142+
raise MissingSliceError.new(slice) unless fs.directory?(slice_directory)
143+
end
144+
end
145+
end
146+
end
147+
end
148+
end
149+
end
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# frozen_string_literal: true
2+
3+
require "erb"
4+
require "dry/files"
5+
require_relative "../constants"
6+
require_relative "../../errors"
7+
8+
module Hanami
9+
module CLI
10+
module Generators
11+
module App
12+
# @since 2.2.0
13+
# @api private
14+
class Struct
15+
# @since 2.2.0
16+
# @api private
17+
def initialize(fs:, inflector:, out: $stdout)
18+
@fs = fs
19+
@inflector = inflector
20+
@out = out
21+
end
22+
23+
# @since 2.2.0
24+
# @api private
25+
def call(app_namespace, key, slice)
26+
RubyFileWriter.new(
27+
fs: fs,
28+
inflector: inflector,
29+
app_namespace: app_namespace,
30+
key: key,
31+
slice: slice,
32+
extra_namespace: "Structs",
33+
relative_parent_class: "DB::Struct",
34+
).call
35+
end
36+
37+
private
38+
39+
attr_reader :fs, :inflector, :out
40+
end
41+
end
42+
end
43+
end
44+
end

spec/unit/hanami/cli/commands/app/generate/operation_spec.rb

+2-3
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,16 @@
33
require "hanami"
44

55
RSpec.describe Hanami::CLI::Commands::App::Generate::Operation, :app do
6-
subject { described_class.new(fs: fs, inflector: inflector, generator_class: generator_class, out: out) }
6+
subject { described_class.new(fs: fs, inflector: inflector, out: out) }
77

88
let(:out) { StringIO.new }
99
let(:fs) { Hanami::CLI::Files.new(memory: true, out: out) }
1010
let(:inflector) { Dry::Inflector.new }
11-
let(:generator_class) { Hanami::CLI::Generators::App::Operation }
1211
let(:app) { Hanami.app.namespace }
1312
let(:dir) { inflector.underscore(app) }
1413

1514
def output
16-
out.rewind && out.read.chomp
15+
out.string.chomp
1716
end
1817

1918
context "generating for app" do

0 commit comments

Comments
 (0)