From d3e5e78be22450d3cfb2e7e6b14d947ade43e5d9 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Fri, 14 Jun 2024 16:01:35 -0600 Subject: [PATCH 01/56] Add dry-operation to default Gemfile --- lib/hanami/cli/generators/gem/app/gemfile.erb | 1 + spec/unit/hanami/cli/commands/gem/new_spec.rb | 2 ++ 2 files changed, 3 insertions(+) diff --git a/lib/hanami/cli/generators/gem/app/gemfile.erb b/lib/hanami/cli/generators/gem/app/gemfile.erb index 3a43f0c0..771359f4 100644 --- a/lib/hanami/cli/generators/gem/app/gemfile.erb +++ b/lib/hanami/cli/generators/gem/app/gemfile.erb @@ -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" diff --git a/spec/unit/hanami/cli/commands/gem/new_spec.rb b/spec/unit/hanami/cli/commands/gem/new_spec.rb index c5d78723..06d34389 100644 --- a/spec/unit/hanami/cli/commands/gem/new_spec.rb +++ b/spec/unit/hanami/cli/commands/gem/new_spec.rb @@ -128,6 +128,7 @@ gem "hanami-view", "#{hanami_version}" gem "dry-types", "~> 1.0", ">= 1.6.1" + gem "dry-operation", github: "dry-rb/dry-operation" gem "puma" gem "rake" @@ -476,6 +477,7 @@ module Types gem "hanami-view", github: "hanami/view", branch: "main" gem "dry-types", "~> 1.0", ">= 1.6.1" + gem "dry-operation", github: "dry-rb/dry-operation" gem "puma" gem "rake" From 888c388d8e75d07c7a30905fafff9b38a0e73dc7 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Fri, 14 Jun 2024 16:12:33 -0600 Subject: [PATCH 02/56] Add base Operation class, based on dry-operation --- lib/hanami/cli/generators/gem/app.rb | 2 ++ lib/hanami/cli/generators/gem/app/operation.erb | 9 +++++++++ spec/unit/hanami/cli/commands/gem/new_spec.rb | 15 +++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 lib/hanami/cli/generators/gem/app/operation.erb diff --git a/lib/hanami/cli/generators/gem/app.rb b/lib/hanami/cli/generators/gem/app.rb index a2b27fd5..440de26b 100644 --- a/lib/hanami/cli/generators/gem/app.rb +++ b/lib/hanami/cli/generators/gem/app.rb @@ -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 diff --git a/lib/hanami/cli/generators/gem/app/operation.erb b/lib/hanami/cli/generators/gem/app/operation.erb new file mode 100644 index 00000000..d0449f2c --- /dev/null +++ b/lib/hanami/cli/generators/gem/app/operation.erb @@ -0,0 +1,9 @@ +# auto_register: false +# frozen_string_literal: true + +require "dry/operation" + +module <%= camelized_app_name %> + class Operation < Dry::Operation + end +end diff --git a/spec/unit/hanami/cli/commands/gem/new_spec.rb b/spec/unit/hanami/cli/commands/gem/new_spec.rb index 06d34389..e476107c 100644 --- a/spec/unit/hanami/cli/commands/gem/new_spec.rb +++ b/spec/unit/hanami/cli/commands/gem/new_spec.rb @@ -436,6 +436,21 @@ module Types expect(fs.read("lib/#{app}/types.rb")).to eq(types) expect(output).to include("Created lib/bookshelf/types.rb") + # app/operation.rb + action = <<~EXPECTED + # auto_register: false + # frozen_string_literal: true + + require "dry/operation" + + module #{inflector.camelize(app)} + class Operation < Dry::Operation + end + end + EXPECTED + expect(fs.read("app/operation.rb")).to eq(action) + expect(output).to include("Created app/operation.rb") + # public/ error pages expect(fs.read("public/404.html")).to include %(The page you were looking for doesn’t exist (404)) expect(fs.read("public/500.html")).to include %(We’re sorry, but something went wrong (500)) From 24d3d6be2f37fc3bc9e78396a5a25a52c35c9f74 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Fri, 14 Jun 2024 17:10:47 -0600 Subject: [PATCH 03/56] Fix view spec --- spec/unit/hanami/cli/commands/app/generate/view_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/unit/hanami/cli/commands/app/generate/view_spec.rb b/spec/unit/hanami/cli/commands/app/generate/view_spec.rb index 33909bf1..4abf30ad 100644 --- a/spec/unit/hanami/cli/commands/app/generate/view_spec.rb +++ b/spec/unit/hanami/cli/commands/app/generate/view_spec.rb @@ -53,7 +53,7 @@ class Index < Test::View EXPECTED expect(fs.read("app/templates/users/index.html.erb")).to eq(template_file) - expect(output).to include("Created app/views/users/index.rb") + expect(output).to include("Created app/templates/users/index.html.erb") end end @@ -88,7 +88,7 @@ class Index < Test::View EXPECTED expect(fs.read("app/templates/special/users/index.html.erb")).to eq(template_file) - expect(output).to include("Created app/views/special/users/index.rb") + expect(output).to include("Created app/templates/special/users/index.html.erb") end end end @@ -124,7 +124,7 @@ class Index < Main::View EXPECTED expect(fs.read("slices/main/templates/users/index.html.erb")).to eq(template_file) - expect(output).to include("Created slices/main/views/users/index.rb") + expect(output).to include("Created slices/main/templates/users/index.html.erb") end end end From bc2670af3f65a64713a3cdeb88b104512335ac01 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Fri, 14 Jun 2024 18:18:42 -0600 Subject: [PATCH 04/56] Add Operation generators --- .../cli/commands/app/generate/operation.rb | 50 ++++++++++ lib/hanami/cli/generators/app/operation.rb | 65 +++++++++++++ .../app/operation/app_operation.erb | 10 ++ .../app/operation/slice_operation.erb | 10 ++ .../cli/generators/app/operation_context.rb | 83 +++++++++++++++++ lib/hanami/cli/generators/app/slice.rb | 1 + .../cli/generators/app/slice/operation.erb | 7 ++ .../commands/app/generate/operation_spec.rb | 91 +++++++++++++++++++ .../cli/commands/app/generate/slice_spec.rb | 12 +++ 9 files changed, 329 insertions(+) create mode 100644 lib/hanami/cli/commands/app/generate/operation.rb create mode 100644 lib/hanami/cli/generators/app/operation.rb create mode 100644 lib/hanami/cli/generators/app/operation/app_operation.erb create mode 100644 lib/hanami/cli/generators/app/operation/slice_operation.erb create mode 100644 lib/hanami/cli/generators/app/operation_context.rb create mode 100644 lib/hanami/cli/generators/app/slice/operation.erb create mode 100644 spec/unit/hanami/cli/commands/app/generate/operation_spec.rb diff --git a/lib/hanami/cli/commands/app/generate/operation.rb b/lib/hanami/cli/commands/app/generate/operation.rb new file mode 100644 index 00000000..c12ece17 --- /dev/null +++ b/lib/hanami/cli/commands/app/generate/operation.rb @@ -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 [ + %(add_book (MyApp::Operations::AddBook)), + %(add_book --slice=admin (Admin::Operations::AddBook)), + ] + 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 diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb new file mode 100644 index 00000000..4efc2baa --- /dev/null +++ b/lib/hanami/cli/generators/app/operation.rb @@ -0,0 +1,65 @@ +# 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) + + fs.mkdir(directory = fs.join(slice_directory, "operations", context.namespaces)) + fs.write(fs.join(directory, "#{context.name}.rb"), t("slice_operation.erb", context)) + end + + def generate_for_app(context) + fs.mkdir(directory = fs.join("app", "operations", context.namespaces)) + fs.write(fs.join(directory, "#{context.name}.rb"), t("app_operation.erb", context)) + 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 diff --git a/lib/hanami/cli/generators/app/operation/app_operation.erb b/lib/hanami/cli/generators/app/operation/app_operation.erb new file mode 100644 index 00000000..3ff89a84 --- /dev/null +++ b/lib/hanami/cli/generators/app/operation/app_operation.erb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module <%= camelized_app_name %> + module Operations +<%= module_namespace_declaration %> +<%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_app_name %>::Operation +<%= module_namespace_offset %>end +<%= module_namespace_end %> + end +end diff --git a/lib/hanami/cli/generators/app/operation/slice_operation.erb b/lib/hanami/cli/generators/app/operation/slice_operation.erb new file mode 100644 index 00000000..0cfd006b --- /dev/null +++ b/lib/hanami/cli/generators/app/operation/slice_operation.erb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module <%= camelized_slice_name %> + module Operations +<%= module_namespace_declaration %> +<%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_slice_name %>::Operation +<%= module_namespace_offset %>end +<%= module_namespace_end %> + end +end diff --git a/lib/hanami/cli/generators/app/operation_context.rb b/lib/hanami/cli/generators/app/operation_context.rb new file mode 100644 index 00000000..6cc09622 --- /dev/null +++ b/lib/hanami/cli/generators/app/operation_context.rb @@ -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 = "." + private_constant :KEY_SEPARATOR + + NAMESPACE_SEPARATOR = "::" + private_constant :NAMESPACE_SEPARATOR + + INDENTATION = " " + private_constant :INDENTATION + + OFFSET = INDENTATION * 2 + 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 diff --git a/lib/hanami/cli/generators/app/slice.rb b/lib/hanami/cli/generators/app/slice.rb index 6d1624e3..fcd90861 100644 --- a/lib/hanami/cli/generators/app/slice.rb +++ b/lib/hanami/cli/generators/app/slice.rb @@ -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)) diff --git a/lib/hanami/cli/generators/app/slice/operation.erb b/lib/hanami/cli/generators/app/slice/operation.erb new file mode 100644 index 00000000..7501188f --- /dev/null +++ b/lib/hanami/cli/generators/app/slice/operation.erb @@ -0,0 +1,7 @@ +# auto_register: false +# frozen_string_literal: true + +module <%= camelized_slice_name %> + class Operation < <%= camelized_app_name %>::Operation + end +end diff --git a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb new file mode 100644 index 00000000..e6b71295 --- /dev/null +++ b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require "hanami" +require "ostruct" + +RSpec.describe Hanami::CLI::Commands::App::Generate::Operation, :app do + subject { described_class.new(fs: fs, inflector: inflector, generator: generator) } + + let(:out) { StringIO.new } + let(:fs) { Hanami::CLI::Files.new(memory: true, out: out) } + let(:inflector) { Dry::Inflector.new } + let(:generator) { Hanami::CLI::Generators::App::Operation.new(fs: fs, inflector: inflector) } + let(:app) { Hanami.app.namespace } + let(:dir) { inflector.underscore(app) } + + def output + out.rewind && out.read.chomp + end + + context "generating for app" do + it "generates an operation" do + subject.call(name: "add_book") + + # operation + operation_file = <<~EXPECTED + # frozen_string_literal: true + + module Test + module Operations + + class AddBook < Test::Operation + end + + end + end + EXPECTED + + expect(fs.read("app/operations/add_book.rb")).to eq(operation_file) + expect(output).to include("Created app/operations/add_book.rb") + end + + xit "add one for slashes? it does compact module syntax... but why?" + + it "generates a operation in a deep namespace" do + subject.call(name: "external.books.add") + + # view + operation_file = <<~EXPECTED + # frozen_string_literal: true + + module Test + module Operations + module External + module Books + class Add < Test::Operation + end + end + end + end + end + EXPECTED + + expect(fs.read("app/operations/external/books/add.rb")).to eq(operation_file) + expect(output).to include("Created app/operations/external/books/add.rb") + end + end + + context "generating for a slice" do + it "generates a operation in a top-level namespace" do + fs.mkdir("slices/main") + subject.call(name: "add_book", slice: "main") + + # operation + operation_file = <<~EXPECTED + # frozen_string_literal: true + + module Main + module Operations + + class AddBook < Main::Operation + end + + end + end + EXPECTED + + expect(fs.read("slices/main/operations/add_book.rb")).to eq(operation_file) + expect(output).to include("Created slices/main/operations/add_book.rb") + end + end +end diff --git a/spec/unit/hanami/cli/commands/app/generate/slice_spec.rb b/spec/unit/hanami/cli/commands/app/generate/slice_spec.rb index 8bd8e99d..c05825f2 100644 --- a/spec/unit/hanami/cli/commands/app/generate/slice_spec.rb +++ b/spec/unit/hanami/cli/commands/app/generate/slice_spec.rb @@ -108,6 +108,18 @@ module Helpers expect(fs.read("slices/#{slice}/views/helpers.rb")).to eq(helpers) expect(output).to include("Created slices/#{slice}/views/helpers.rb") + operation = <<~RUBY + # auto_register: false + # frozen_string_literal: true + + module Admin + class Operation < Bookshelf::Operation + end + end + RUBY + expect(fs.read("slices/#{slice}/operation.rb")).to eq(operation) + expect(output).to include("Created slices/#{slice}/operation.rb") + layout = <<~ERB From 1c37df3cbe6696ae3700601b2599e8a03bb9e6dd Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Fri, 14 Jun 2024 18:59:48 -0600 Subject: [PATCH 05/56] Add empty `call` method definition --- lib/hanami/cli/generators/app/operation/app_operation.erb | 2 ++ lib/hanami/cli/generators/app/operation/slice_operation.erb | 2 ++ .../unit/hanami/cli/commands/app/generate/operation_spec.rb | 6 ++++++ 3 files changed, 10 insertions(+) diff --git a/lib/hanami/cli/generators/app/operation/app_operation.erb b/lib/hanami/cli/generators/app/operation/app_operation.erb index 3ff89a84..100ecc68 100644 --- a/lib/hanami/cli/generators/app/operation/app_operation.erb +++ b/lib/hanami/cli/generators/app/operation/app_operation.erb @@ -4,6 +4,8 @@ module <%= camelized_app_name %> module Operations <%= module_namespace_declaration %> <%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_app_name %>::Operation +<%= module_namespace_offset %> def call(input) +<%= module_namespace_offset %> end <%= module_namespace_offset %>end <%= module_namespace_end %> end diff --git a/lib/hanami/cli/generators/app/operation/slice_operation.erb b/lib/hanami/cli/generators/app/operation/slice_operation.erb index 0cfd006b..b3faf784 100644 --- a/lib/hanami/cli/generators/app/operation/slice_operation.erb +++ b/lib/hanami/cli/generators/app/operation/slice_operation.erb @@ -4,6 +4,8 @@ module <%= camelized_slice_name %> module Operations <%= module_namespace_declaration %> <%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_slice_name %>::Operation +<%= module_namespace_offset %> def call(input) +<%= module_namespace_offset %> end <%= module_namespace_offset %>end <%= module_namespace_end %> end diff --git a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb index e6b71295..beb13297 100644 --- a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb +++ b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb @@ -29,6 +29,8 @@ module Test module Operations class AddBook < Test::Operation + def call(input) + end end end @@ -53,6 +55,8 @@ module Operations module External module Books class Add < Test::Operation + def call(input) + end end end end @@ -78,6 +82,8 @@ module Main module Operations class AddBook < Main::Operation + def call(input) + end end end From a3f6a037f308faff85cd112980bdadde1a06697a Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Fri, 14 Jun 2024 19:01:06 -0600 Subject: [PATCH 06/56] Remove ostruct --- spec/unit/hanami/cli/commands/app/generate/operation_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb index beb13297..be3f5be6 100644 --- a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb +++ b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "hanami" -require "ostruct" RSpec.describe Hanami::CLI::Commands::App::Generate::Operation, :app do subject { described_class.new(fs: fs, inflector: inflector, generator: generator) } From 649bdcb03a90cf9a34fd703af0ac08afad069bd4 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 17 Jun 2024 12:55:02 -0600 Subject: [PATCH 07/56] Allow slash separator for generator --- .../cli/generators/app/operation_context.rb | 2 +- .../commands/app/generate/operation_spec.rb | 25 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/hanami/cli/generators/app/operation_context.rb b/lib/hanami/cli/generators/app/operation_context.rb index 6cc09622..72bac616 100644 --- a/lib/hanami/cli/generators/app/operation_context.rb +++ b/lib/hanami/cli/generators/app/operation_context.rb @@ -13,7 +13,7 @@ module App # @api private class OperationContext < SliceContext # TODO: move these constants somewhere that will let us reuse them - KEY_SEPARATOR = "." + KEY_SEPARATOR = %r{\.|/} private_constant :KEY_SEPARATOR NAMESPACE_SEPARATOR = "::" diff --git a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb index be3f5be6..502045ea 100644 --- a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb +++ b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb @@ -40,7 +40,30 @@ def call(input) expect(output).to include("Created app/operations/add_book.rb") end - xit "add one for slashes? it does compact module syntax... but why?" + it "add one for slashes? it does compact module syntax... but why?" do + subject.call(name: "external/books/add") + + # operation + operation_file = <<~EXPECTED + # frozen_string_literal: true + + module Test + module Operations + module External + module Books + class Add < Test::Operation + def call(input) + end + end + end + end + end + end + EXPECTED + + expect(fs.read("app/operations/external/books/add.rb")).to eq(operation_file) + expect(output).to include("Created app/operations/external/books/add.rb") + end it "generates a operation in a deep namespace" do subject.call(name: "external.books.add") From f74519fda993ab2979d39ed9e690edb6f637b27e Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 17 Jun 2024 12:55:42 -0600 Subject: [PATCH 08/56] Allow slash separator for generator --- .../cli/commands/app/generate/operation_spec.rb | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb index 502045ea..6119c818 100644 --- a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb +++ b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb @@ -20,7 +20,6 @@ def output it "generates an operation" do subject.call(name: "add_book") - # operation operation_file = <<~EXPECTED # frozen_string_literal: true @@ -40,10 +39,9 @@ def call(input) expect(output).to include("Created app/operations/add_book.rb") end - it "add one for slashes? it does compact module syntax... but why?" do - subject.call(name: "external/books/add") + it "generates a operation in a deep namespace with default separator" do + subject.call(name: "external.books.add") - # operation operation_file = <<~EXPECTED # frozen_string_literal: true @@ -65,10 +63,9 @@ def call(input) expect(output).to include("Created app/operations/external/books/add.rb") end - it "generates a operation in a deep namespace" do - subject.call(name: "external.books.add") + it "generates an operation in a deep namespace with slash separators" do + subject.call(name: "external/books/add") - # view operation_file = <<~EXPECTED # frozen_string_literal: true @@ -96,7 +93,6 @@ def call(input) fs.mkdir("slices/main") subject.call(name: "add_book", slice: "main") - # operation operation_file = <<~EXPECTED # frozen_string_literal: true From 0f9f81494b630db02f019e78ec002b7ec0914412 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 17 Jun 2024 12:56:39 -0600 Subject: [PATCH 09/56] Rename module to admin --- .../cli/commands/app/generate/operation_spec.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb index 6119c818..e16b0808 100644 --- a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb +++ b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb @@ -40,14 +40,14 @@ def call(input) end it "generates a operation in a deep namespace with default separator" do - subject.call(name: "external.books.add") + subject.call(name: "admin.books.add") operation_file = <<~EXPECTED # frozen_string_literal: true module Test module Operations - module External + module Admin module Books class Add < Test::Operation def call(input) @@ -59,19 +59,19 @@ def call(input) end EXPECTED - expect(fs.read("app/operations/external/books/add.rb")).to eq(operation_file) - expect(output).to include("Created app/operations/external/books/add.rb") + expect(fs.read("app/operations/admin/books/add.rb")).to eq(operation_file) + expect(output).to include("Created app/operations/admin/books/add.rb") end it "generates an operation in a deep namespace with slash separators" do - subject.call(name: "external/books/add") + subject.call(name: "admin/books/add") operation_file = <<~EXPECTED # frozen_string_literal: true module Test module Operations - module External + module Admin module Books class Add < Test::Operation def call(input) @@ -83,8 +83,8 @@ def call(input) end EXPECTED - expect(fs.read("app/operations/external/books/add.rb")).to eq(operation_file) - expect(output).to include("Created app/operations/external/books/add.rb") + expect(fs.read("app/operations/admin/books/add.rb")).to eq(operation_file) + expect(output).to include("Created app/operations/admin/books/add.rb") end end From 663abc6193fccdfa0e3bbfd424644bb16fcbd1e6 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 17 Jun 2024 13:21:52 -0600 Subject: [PATCH 10/56] Remove newlines in generated files By adding new templates for un-nested operations --- lib/hanami/cli/generators/app/operation.rb | 18 ++++++++++++++---- ..._operation.erb => nested_app_operation.erb} | 0 ...peration.erb => nested_slice_operation.erb} | 0 .../app/operation/top_level_app_operation.erb | 10 ++++++++++ .../operation/top_level_slice_operation.erb | 10 ++++++++++ .../commands/app/generate/operation_spec.rb | 4 ---- 6 files changed, 34 insertions(+), 8 deletions(-) rename lib/hanami/cli/generators/app/operation/{app_operation.erb => nested_app_operation.erb} (100%) rename lib/hanami/cli/generators/app/operation/{slice_operation.erb => nested_slice_operation.erb} (100%) create mode 100644 lib/hanami/cli/generators/app/operation/top_level_app_operation.erb create mode 100644 lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index 4efc2baa..5d56840d 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -40,13 +40,23 @@ def generate_for_slice(context, slice) slice_directory = fs.join("slices", slice) raise MissingSliceError.new(slice) unless fs.directory?(slice_directory) - fs.mkdir(directory = fs.join(slice_directory, "operations", context.namespaces)) - fs.write(fs.join(directory, "#{context.name}.rb"), t("slice_operation.erb", context)) + if context.namespaces.any? + fs.mkdir(directory = fs.join(slice_directory, "operations", context.namespaces)) + fs.write(fs.join(directory, "#{context.name}.rb"), t("nested_slice_operation.erb", context)) + else + fs.mkdir(directory = fs.join(slice_directory, "operations")) + fs.write(fs.join(directory, "#{context.name}.rb"), t("top_level_slice_operation.erb", context)) + end end def generate_for_app(context) - fs.mkdir(directory = fs.join("app", "operations", context.namespaces)) - fs.write(fs.join(directory, "#{context.name}.rb"), t("app_operation.erb", context)) + if context.namespaces.any? + fs.mkdir(directory = fs.join("app", "operations", context.namespaces)) + fs.write(fs.join(directory, "#{context.name}.rb"), t("nested_app_operation.erb", context)) + else + fs.mkdir(directory = fs.join("app", "operations")) + fs.write(fs.join(directory, "#{context.name}.rb"), t("top_level_app_operation.erb", context)) + end end def template(path, context) diff --git a/lib/hanami/cli/generators/app/operation/app_operation.erb b/lib/hanami/cli/generators/app/operation/nested_app_operation.erb similarity index 100% rename from lib/hanami/cli/generators/app/operation/app_operation.erb rename to lib/hanami/cli/generators/app/operation/nested_app_operation.erb diff --git a/lib/hanami/cli/generators/app/operation/slice_operation.erb b/lib/hanami/cli/generators/app/operation/nested_slice_operation.erb similarity index 100% rename from lib/hanami/cli/generators/app/operation/slice_operation.erb rename to lib/hanami/cli/generators/app/operation/nested_slice_operation.erb diff --git a/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb b/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb new file mode 100644 index 00000000..12776ce2 --- /dev/null +++ b/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module <%= camelized_app_name %> + module Operations +<%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_app_name %>::Operation +<%= module_namespace_offset %> def call(input) +<%= module_namespace_offset %> end +<%= module_namespace_offset %>end + end +end diff --git a/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb b/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb new file mode 100644 index 00000000..a9ec1cd9 --- /dev/null +++ b/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module <%= camelized_slice_name %> + module Operations +<%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_slice_name %>::Operation +<%= module_namespace_offset %> def call(input) +<%= module_namespace_offset %> end +<%= module_namespace_offset %>end + end +end diff --git a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb index e16b0808..c6b7579a 100644 --- a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb +++ b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb @@ -25,12 +25,10 @@ def output module Test module Operations - class AddBook < Test::Operation def call(input) end end - end end EXPECTED @@ -98,12 +96,10 @@ def call(input) module Main module Operations - class AddBook < Main::Operation def call(input) end end - end end EXPECTED From 3b72feb279a49538a29ea53c0da8d0ea77fd3222 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Wed, 19 Jun 2024 12:20:06 -0600 Subject: [PATCH 11/56] Remove input as default args --- .../cli/generators/app/operation/nested_app_operation.erb | 2 +- .../generators/app/operation/nested_slice_operation.erb | 2 +- .../generators/app/operation/top_level_app_operation.erb | 2 +- .../app/operation/top_level_slice_operation.erb | 2 +- .../hanami/cli/commands/app/generate/operation_spec.rb | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/hanami/cli/generators/app/operation/nested_app_operation.erb b/lib/hanami/cli/generators/app/operation/nested_app_operation.erb index 100ecc68..208822a0 100644 --- a/lib/hanami/cli/generators/app/operation/nested_app_operation.erb +++ b/lib/hanami/cli/generators/app/operation/nested_app_operation.erb @@ -4,7 +4,7 @@ module <%= camelized_app_name %> module Operations <%= module_namespace_declaration %> <%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_app_name %>::Operation -<%= module_namespace_offset %> def call(input) +<%= module_namespace_offset %> def call <%= module_namespace_offset %> end <%= module_namespace_offset %>end <%= module_namespace_end %> diff --git a/lib/hanami/cli/generators/app/operation/nested_slice_operation.erb b/lib/hanami/cli/generators/app/operation/nested_slice_operation.erb index b3faf784..09a23e1e 100644 --- a/lib/hanami/cli/generators/app/operation/nested_slice_operation.erb +++ b/lib/hanami/cli/generators/app/operation/nested_slice_operation.erb @@ -4,7 +4,7 @@ module <%= camelized_slice_name %> module Operations <%= module_namespace_declaration %> <%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_slice_name %>::Operation -<%= module_namespace_offset %> def call(input) +<%= module_namespace_offset %> def call <%= module_namespace_offset %> end <%= module_namespace_offset %>end <%= module_namespace_end %> diff --git a/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb b/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb index 12776ce2..831fafce 100644 --- a/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb +++ b/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb @@ -3,7 +3,7 @@ module <%= camelized_app_name %> module Operations <%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_app_name %>::Operation -<%= module_namespace_offset %> def call(input) +<%= module_namespace_offset %> def call <%= module_namespace_offset %> end <%= module_namespace_offset %>end end diff --git a/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb b/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb index a9ec1cd9..7671780d 100644 --- a/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb +++ b/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb @@ -3,7 +3,7 @@ module <%= camelized_slice_name %> module Operations <%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_slice_name %>::Operation -<%= module_namespace_offset %> def call(input) +<%= module_namespace_offset %> def call <%= module_namespace_offset %> end <%= module_namespace_offset %>end end diff --git a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb index c6b7579a..ea29c7da 100644 --- a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb +++ b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb @@ -26,7 +26,7 @@ def output module Test module Operations class AddBook < Test::Operation - def call(input) + def call end end end @@ -48,7 +48,7 @@ module Operations module Admin module Books class Add < Test::Operation - def call(input) + def call end end end @@ -72,7 +72,7 @@ module Operations module Admin module Books class Add < Test::Operation - def call(input) + def call end end end @@ -97,7 +97,7 @@ def call(input) module Main module Operations class AddBook < Main::Operation - def call(input) + def call end end end From 0f81a5c58ca486d231fd82f724b6c2be8410c3f5 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Wed, 19 Jun 2024 12:35:43 -0600 Subject: [PATCH 12/56] Remove Operations namespace, generate in app/ or slices/SLICE_NAME/ --- lib/hanami/cli/generators/app/operation.rb | 8 ++-- .../app/operation/nested_app_operation.erb | 2 - .../app/operation/nested_slice_operation.erb | 2 - .../app/operation/top_level_app_operation.erb | 2 - .../operation/top_level_slice_operation.erb | 2 - .../cli/generators/app/operation_context.rb | 2 +- .../commands/app/generate/operation_spec.rb | 48 ++++++++----------- 7 files changed, 25 insertions(+), 41 deletions(-) diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index 5d56840d..559af8c2 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -41,20 +41,20 @@ def generate_for_slice(context, slice) raise MissingSliceError.new(slice) unless fs.directory?(slice_directory) if context.namespaces.any? - fs.mkdir(directory = fs.join(slice_directory, "operations", context.namespaces)) + 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, "operations")) + fs.mkdir(directory = fs.join(slice_directory)) fs.write(fs.join(directory, "#{context.name}.rb"), t("top_level_slice_operation.erb", context)) end end def generate_for_app(context) if context.namespaces.any? - fs.mkdir(directory = fs.join("app", "operations", context.namespaces)) + 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", "operations")) + fs.mkdir(directory = fs.join("app")) fs.write(fs.join(directory, "#{context.name}.rb"), t("top_level_app_operation.erb", context)) end end diff --git a/lib/hanami/cli/generators/app/operation/nested_app_operation.erb b/lib/hanami/cli/generators/app/operation/nested_app_operation.erb index 208822a0..f0b7a131 100644 --- a/lib/hanami/cli/generators/app/operation/nested_app_operation.erb +++ b/lib/hanami/cli/generators/app/operation/nested_app_operation.erb @@ -1,12 +1,10 @@ # frozen_string_literal: true module <%= camelized_app_name %> - module Operations <%= 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 end diff --git a/lib/hanami/cli/generators/app/operation/nested_slice_operation.erb b/lib/hanami/cli/generators/app/operation/nested_slice_operation.erb index 09a23e1e..a9a448ad 100644 --- a/lib/hanami/cli/generators/app/operation/nested_slice_operation.erb +++ b/lib/hanami/cli/generators/app/operation/nested_slice_operation.erb @@ -1,12 +1,10 @@ # frozen_string_literal: true module <%= camelized_slice_name %> - module Operations <%= 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 end diff --git a/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb b/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb index 831fafce..0ce59f47 100644 --- a/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb +++ b/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb @@ -1,10 +1,8 @@ # frozen_string_literal: true module <%= camelized_app_name %> - module Operations <%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_app_name %>::Operation <%= module_namespace_offset %> def call <%= module_namespace_offset %> end <%= module_namespace_offset %>end - end end diff --git a/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb b/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb index 7671780d..240448ab 100644 --- a/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb +++ b/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb @@ -1,10 +1,8 @@ # frozen_string_literal: true module <%= camelized_slice_name %> - module Operations <%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_slice_name %>::Operation <%= module_namespace_offset %> def call <%= module_namespace_offset %> end <%= module_namespace_offset %>end - end end diff --git a/lib/hanami/cli/generators/app/operation_context.rb b/lib/hanami/cli/generators/app/operation_context.rb index 72bac616..6e17bdfb 100644 --- a/lib/hanami/cli/generators/app/operation_context.rb +++ b/lib/hanami/cli/generators/app/operation_context.rb @@ -22,7 +22,7 @@ class OperationContext < SliceContext INDENTATION = " " private_constant :INDENTATION - OFFSET = INDENTATION * 2 + OFFSET = INDENTATION private_constant :OFFSET # @since x.x.x diff --git a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb index ea29c7da..be5f8c0d 100644 --- a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb +++ b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb @@ -24,17 +24,15 @@ def output # frozen_string_literal: true module Test - module Operations - class AddBook < Test::Operation - def call - end + class AddBook < Test::Operation + def call end end end EXPECTED - expect(fs.read("app/operations/add_book.rb")).to eq(operation_file) - expect(output).to include("Created app/operations/add_book.rb") + expect(fs.read("app/add_book.rb")).to eq(operation_file) + expect(output).to include("Created app/add_book.rb") end it "generates a operation in a deep namespace with default separator" do @@ -44,12 +42,10 @@ def call # frozen_string_literal: true module Test - module Operations - module Admin - module Books - class Add < Test::Operation - def call - end + module Admin + module Books + class Add < Test::Operation + def call end end end @@ -57,8 +53,8 @@ def call end EXPECTED - expect(fs.read("app/operations/admin/books/add.rb")).to eq(operation_file) - expect(output).to include("Created app/operations/admin/books/add.rb") + expect(fs.read("app/admin/books/add.rb")).to eq(operation_file) + expect(output).to include("Created app/admin/books/add.rb") end it "generates an operation in a deep namespace with slash separators" do @@ -68,12 +64,10 @@ def call # frozen_string_literal: true module Test - module Operations - module Admin - module Books - class Add < Test::Operation - def call - end + module Admin + module Books + class Add < Test::Operation + def call end end end @@ -81,8 +75,8 @@ def call end EXPECTED - expect(fs.read("app/operations/admin/books/add.rb")).to eq(operation_file) - expect(output).to include("Created app/operations/admin/books/add.rb") + expect(fs.read("app/admin/books/add.rb")).to eq(operation_file) + expect(output).to include("Created app/admin/books/add.rb") end end @@ -95,17 +89,15 @@ def call # frozen_string_literal: true module Main - module Operations - class AddBook < Main::Operation - def call - end + class AddBook < Main::Operation + def call end end end EXPECTED - expect(fs.read("slices/main/operations/add_book.rb")).to eq(operation_file) - expect(output).to include("Created slices/main/operations/add_book.rb") + expect(fs.read("slices/main/add_book.rb")).to eq(operation_file) + expect(output).to include("Created slices/main/add_book.rb") end end end From a5bd2f30311293c24594d4c84bb0c1033dd055e3 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Wed, 19 Jun 2024 14:16:45 -0600 Subject: [PATCH 13/56] Prevent generating operation without namespace --- lib/hanami/cli/errors.rb | 4 + lib/hanami/cli/generators/app/operation.rb | 20 +++-- ...ed_app_operation.erb => app_operation.erb} | 0 ...lice_operation.erb => slice_operation.erb} | 0 .../app/operation/top_level_app_operation.erb | 8 -- .../operation/top_level_slice_operation.erb | 8 -- .../commands/app/generate/operation_spec.rb | 78 ++++++++++++++++--- 7 files changed, 84 insertions(+), 34 deletions(-) rename lib/hanami/cli/generators/app/operation/{nested_app_operation.erb => app_operation.erb} (100%) rename lib/hanami/cli/generators/app/operation/{nested_slice_operation.erb => slice_operation.erb} (100%) delete mode 100644 lib/hanami/cli/generators/app/operation/top_level_app_operation.erb delete mode 100644 lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb diff --git a/lib/hanami/cli/errors.rb b/lib/hanami/cli/errors.rb index 235ad771..5a69842e 100644 --- a/lib/hanami/cli/errors.rb +++ b/lib/hanami/cli/errors.rb @@ -91,5 +91,9 @@ def initialize(scheme) super("`#{scheme}' is not a supported db scheme") end end + + # @since x.x.x + # @api public + class NameNeedsNamespaceError < Error; end end end diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index 559af8c2..88c309b8 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -42,20 +42,18 @@ def generate_for_slice(context, slice) 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)) + fs.write(fs.join(directory, "#{context.name}.rb"), t("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)) + print_error_message_about_naming(context.name, slice_directory) 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)) + fs.write(fs.join(directory, "#{context.name}.rb"), t("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)) + print_error_message_about_naming(context.name, "app") end end @@ -67,6 +65,16 @@ def template(path, context) ).result(context.ctx) end + def print_error_message_about_naming(provided_name, base_location) + raise NameNeedsNamespaceError.new( + "Failed to create operation `#{provided_name}'. " \ + "This would create the operation directly in the `#{base_location}/' folder. " \ + "Instead, you should provide a namespace for the folder where this operation will live. " \ + "NOTE: We recommend giving it a name that's specific to your domain, " \ + "but you can also use `operations.#{provided_name}' in the meantime if you're unsure." + ) + end + alias_method :t, :template end end diff --git a/lib/hanami/cli/generators/app/operation/nested_app_operation.erb b/lib/hanami/cli/generators/app/operation/app_operation.erb similarity index 100% rename from lib/hanami/cli/generators/app/operation/nested_app_operation.erb rename to lib/hanami/cli/generators/app/operation/app_operation.erb diff --git a/lib/hanami/cli/generators/app/operation/nested_slice_operation.erb b/lib/hanami/cli/generators/app/operation/slice_operation.erb similarity index 100% rename from lib/hanami/cli/generators/app/operation/nested_slice_operation.erb rename to lib/hanami/cli/generators/app/operation/slice_operation.erb diff --git a/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb b/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb deleted file mode 100644 index 0ce59f47..00000000 --- a/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb +++ /dev/null @@ -1,8 +0,0 @@ -# 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 diff --git a/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb b/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb deleted file mode 100644 index 240448ab..00000000 --- a/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb +++ /dev/null @@ -1,8 +0,0 @@ -# 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 diff --git a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb index be5f8c0d..5529d230 100644 --- a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb +++ b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb @@ -18,21 +18,23 @@ def output context "generating for app" do it "generates an operation" do - subject.call(name: "add_book") + subject.call(name: "operations/add_book") operation_file = <<~EXPECTED # frozen_string_literal: true module Test - class AddBook < Test::Operation - def call + module Operations + class AddBook < Test::Operation + def call + end end end end EXPECTED - expect(fs.read("app/add_book.rb")).to eq(operation_file) - expect(output).to include("Created app/add_book.rb") + expect(fs.read("app/operations/add_book.rb")).to eq(operation_file) + expect(output).to include("Created app/operations/add_book.rb") end it "generates a operation in a deep namespace with default separator" do @@ -57,7 +59,7 @@ def call expect(output).to include("Created app/admin/books/add.rb") end - it "generates an operation in a deep namespace with slash separators" do + it "generates an operation in a deep namespace with slash separator" do subject.call(name: "admin/books/add") operation_file = <<~EXPECTED @@ -78,26 +80,78 @@ def call expect(fs.read("app/admin/books/add.rb")).to eq(operation_file) expect(output).to include("Created app/admin/books/add.rb") end + + it "outputs an error if trying to generate an operation without a separator" do + expect { + subject.call(name: "add_book") + }.to raise_error(Hanami::CLI::NameNeedsNamespaceError).with_message( + "Failed to create operation `add_book'. " \ + "This would create the operation directly in the `app/' folder. " \ + "Instead, you should provide a namespace for the folder where this operation will live. " \ + "NOTE: We recommend giving it a name that's specific to your domain, " \ + "but you can also use `operations.add_book' in the meantime if you're unsure." + ) + expect(fs.exist?("app/add_book.rb")).to be(false) + end end context "generating for a slice" do - it "generates a operation in a top-level namespace" do + it "generates a operation" do + fs.mkdir("slices/main") + subject.call(name: "operations.add_book", slice: "main") + + operation_file = <<~EXPECTED + # frozen_string_literal: true + + module Main + module Operations + class AddBook < Main::Operation + def call + end + end + end + end + EXPECTED + + expect(fs.read("slices/main/operations/add_book.rb")).to eq(operation_file) + expect(output).to include("Created slices/main/operations/add_book.rb") + end + + it "generates a operation in a deep namespace with default separator" do fs.mkdir("slices/main") - subject.call(name: "add_book", slice: "main") + subject.call(name: "admin.books.add", slice: "main") operation_file = <<~EXPECTED # frozen_string_literal: true module Main - class AddBook < Main::Operation - def call + module Admin + module Books + class Add < Main::Operation + def call + end + end end end end EXPECTED - expect(fs.read("slices/main/add_book.rb")).to eq(operation_file) - expect(output).to include("Created slices/main/add_book.rb") + expect(fs.read("slices/main/admin/books/add.rb")).to eq(operation_file) + expect(output).to include("Created slices/main/admin/books/add.rb") + end + + it "outputs an error if trying to generate an operation without a separator" do + fs.mkdir("slices/main") + expect { + subject.call(name: "add_book", slice: "main") + }.to raise_error(Hanami::CLI::NameNeedsNamespaceError).with_message( + "Failed to create operation `add_book'. " \ + "This would create the operation directly in the `slices/main/' folder. " \ + "Instead, you should provide a namespace for the folder where this operation will live. " \ + "NOTE: We recommend giving it a name that's specific to your domain, " \ + "but you can also use `operations.add_book' in the meantime if you're unsure." + ) + expect(fs.exist?("app/add_book.rb")).to be(false) end end end From eb391ca845404ed25638317392fbc9c3566f96eb Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Thu, 20 Jun 2024 10:20:59 -0600 Subject: [PATCH 14/56] Revert "Prevent generating operation without namespace" This reverts commit a5bd2f30311293c24594d4c84bb0c1033dd055e3. --- lib/hanami/cli/errors.rb | 4 - lib/hanami/cli/generators/app/operation.rb | 20 ++--- ...operation.erb => nested_app_operation.erb} | 0 ...eration.erb => nested_slice_operation.erb} | 0 .../app/operation/top_level_app_operation.erb | 8 ++ .../operation/top_level_slice_operation.erb | 8 ++ .../commands/app/generate/operation_spec.rb | 78 +++---------------- 7 files changed, 34 insertions(+), 84 deletions(-) rename lib/hanami/cli/generators/app/operation/{app_operation.erb => nested_app_operation.erb} (100%) rename lib/hanami/cli/generators/app/operation/{slice_operation.erb => nested_slice_operation.erb} (100%) create mode 100644 lib/hanami/cli/generators/app/operation/top_level_app_operation.erb create mode 100644 lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb diff --git a/lib/hanami/cli/errors.rb b/lib/hanami/cli/errors.rb index 5a69842e..235ad771 100644 --- a/lib/hanami/cli/errors.rb +++ b/lib/hanami/cli/errors.rb @@ -91,9 +91,5 @@ def initialize(scheme) super("`#{scheme}' is not a supported db scheme") end end - - # @since x.x.x - # @api public - class NameNeedsNamespaceError < Error; end end end diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index 88c309b8..559af8c2 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -42,18 +42,20 @@ def generate_for_slice(context, slice) if context.namespaces.any? fs.mkdir(directory = fs.join(slice_directory, context.namespaces)) - fs.write(fs.join(directory, "#{context.name}.rb"), t("slice_operation.erb", context)) + fs.write(fs.join(directory, "#{context.name}.rb"), t("nested_slice_operation.erb", context)) else - print_error_message_about_naming(context.name, slice_directory) + fs.mkdir(directory = fs.join(slice_directory)) + fs.write(fs.join(directory, "#{context.name}.rb"), t("top_level_slice_operation.erb", context)) 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("app_operation.erb", context)) + fs.write(fs.join(directory, "#{context.name}.rb"), t("nested_app_operation.erb", context)) else - print_error_message_about_naming(context.name, "app") + fs.mkdir(directory = fs.join("app")) + fs.write(fs.join(directory, "#{context.name}.rb"), t("top_level_app_operation.erb", context)) end end @@ -65,16 +67,6 @@ def template(path, context) ).result(context.ctx) end - def print_error_message_about_naming(provided_name, base_location) - raise NameNeedsNamespaceError.new( - "Failed to create operation `#{provided_name}'. " \ - "This would create the operation directly in the `#{base_location}/' folder. " \ - "Instead, you should provide a namespace for the folder where this operation will live. " \ - "NOTE: We recommend giving it a name that's specific to your domain, " \ - "but you can also use `operations.#{provided_name}' in the meantime if you're unsure." - ) - end - alias_method :t, :template end end diff --git a/lib/hanami/cli/generators/app/operation/app_operation.erb b/lib/hanami/cli/generators/app/operation/nested_app_operation.erb similarity index 100% rename from lib/hanami/cli/generators/app/operation/app_operation.erb rename to lib/hanami/cli/generators/app/operation/nested_app_operation.erb diff --git a/lib/hanami/cli/generators/app/operation/slice_operation.erb b/lib/hanami/cli/generators/app/operation/nested_slice_operation.erb similarity index 100% rename from lib/hanami/cli/generators/app/operation/slice_operation.erb rename to lib/hanami/cli/generators/app/operation/nested_slice_operation.erb diff --git a/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb b/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb new file mode 100644 index 00000000..0ce59f47 --- /dev/null +++ b/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb @@ -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 diff --git a/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb b/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb new file mode 100644 index 00000000..240448ab --- /dev/null +++ b/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb @@ -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 diff --git a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb index 5529d230..be5f8c0d 100644 --- a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb +++ b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb @@ -18,23 +18,21 @@ def output context "generating for app" do it "generates an operation" do - subject.call(name: "operations/add_book") + subject.call(name: "add_book") operation_file = <<~EXPECTED # frozen_string_literal: true module Test - module Operations - class AddBook < Test::Operation - def call - end + class AddBook < Test::Operation + def call end end end EXPECTED - expect(fs.read("app/operations/add_book.rb")).to eq(operation_file) - expect(output).to include("Created app/operations/add_book.rb") + expect(fs.read("app/add_book.rb")).to eq(operation_file) + expect(output).to include("Created app/add_book.rb") end it "generates a operation in a deep namespace with default separator" do @@ -59,7 +57,7 @@ def call expect(output).to include("Created app/admin/books/add.rb") end - it "generates an operation in a deep namespace with slash separator" do + it "generates an operation in a deep namespace with slash separators" do subject.call(name: "admin/books/add") operation_file = <<~EXPECTED @@ -80,78 +78,26 @@ def call expect(fs.read("app/admin/books/add.rb")).to eq(operation_file) expect(output).to include("Created app/admin/books/add.rb") end - - it "outputs an error if trying to generate an operation without a separator" do - expect { - subject.call(name: "add_book") - }.to raise_error(Hanami::CLI::NameNeedsNamespaceError).with_message( - "Failed to create operation `add_book'. " \ - "This would create the operation directly in the `app/' folder. " \ - "Instead, you should provide a namespace for the folder where this operation will live. " \ - "NOTE: We recommend giving it a name that's specific to your domain, " \ - "but you can also use `operations.add_book' in the meantime if you're unsure." - ) - expect(fs.exist?("app/add_book.rb")).to be(false) - end end context "generating for a slice" do - it "generates a operation" do - fs.mkdir("slices/main") - subject.call(name: "operations.add_book", slice: "main") - - operation_file = <<~EXPECTED - # frozen_string_literal: true - - module Main - module Operations - class AddBook < Main::Operation - def call - end - end - end - end - EXPECTED - - expect(fs.read("slices/main/operations/add_book.rb")).to eq(operation_file) - expect(output).to include("Created slices/main/operations/add_book.rb") - end - - it "generates a operation in a deep namespace with default separator" do + it "generates a operation in a top-level namespace" do fs.mkdir("slices/main") - subject.call(name: "admin.books.add", slice: "main") + subject.call(name: "add_book", slice: "main") operation_file = <<~EXPECTED # frozen_string_literal: true module Main - module Admin - module Books - class Add < Main::Operation - def call - end - end + class AddBook < Main::Operation + def call end end end EXPECTED - expect(fs.read("slices/main/admin/books/add.rb")).to eq(operation_file) - expect(output).to include("Created slices/main/admin/books/add.rb") - end - - it "outputs an error if trying to generate an operation without a separator" do - fs.mkdir("slices/main") - expect { - subject.call(name: "add_book", slice: "main") - }.to raise_error(Hanami::CLI::NameNeedsNamespaceError).with_message( - "Failed to create operation `add_book'. " \ - "This would create the operation directly in the `slices/main/' folder. " \ - "Instead, you should provide a namespace for the folder where this operation will live. " \ - "NOTE: We recommend giving it a name that's specific to your domain, " \ - "but you can also use `operations.add_book' in the meantime if you're unsure." - ) - expect(fs.exist?("app/add_book.rb")).to be(false) + expect(fs.read("slices/main/add_book.rb")).to eq(operation_file) + expect(output).to include("Created slices/main/add_book.rb") end end end From 10232259fb6262d01288644d276b55d19104acd6 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Thu, 20 Jun 2024 10:42:28 -0600 Subject: [PATCH 15/56] Add recommendation to add namespace to operations --- lib/hanami/cli/files.rb | 6 ++++ lib/hanami/cli/generators/app/operation.rb | 2 ++ .../commands/app/generate/operation_spec.rb | 29 +++++++++++++++++-- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/hanami/cli/files.rb b/lib/hanami/cli/files.rb index eb969e5c..bceb568b 100644 --- a/lib/hanami/cli/files.rb +++ b/lib/hanami/cli/files.rb @@ -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 diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index 559af8c2..ec9b381d 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -46,6 +46,7 @@ def generate_for_slice(context, slice) 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}/.") end end @@ -56,6 +57,7 @@ def generate_for_app(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 diff --git a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb index be5f8c0d..09a571d8 100644 --- a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb +++ b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb @@ -17,7 +17,7 @@ def output end context "generating for app" do - it "generates an operation" do + it "generates an operation without a namespace, with a recommendation" do subject.call(name: "add_book") operation_file = <<~EXPECTED @@ -33,6 +33,7 @@ def call expect(fs.read("app/add_book.rb")).to eq(operation_file) expect(output).to include("Created app/add_book.rb") + expect(output).to include(" Recommendation: Add a namespace to operation names, so they go into a folder within app/") end it "generates a operation in a deep namespace with default separator" do @@ -81,7 +82,7 @@ def call end context "generating for a slice" do - it "generates a operation in a top-level namespace" do + it "generates a operation in a top-level namespace, with recommendation" do fs.mkdir("slices/main") subject.call(name: "add_book", slice: "main") @@ -98,6 +99,30 @@ def call expect(fs.read("slices/main/add_book.rb")).to eq(operation_file) expect(output).to include("Created slices/main/add_book.rb") + expect(output).to include(" Recommendation: Add a namespace to operation names, so they go into a folder within slices/main/") + end + + it "generates a operation in a nested namespace" do + fs.mkdir("slices/main") + subject.call(name: "admin.books.add", slice: "main") + + operation_file = <<~EXPECTED + # frozen_string_literal: true + + module Main + module Admin + module Books + class Add < Main::Operation + def call + end + end + end + end + end + EXPECTED + + expect(fs.read("slices/main/admin/books/add.rb")).to eq(operation_file) + expect(output).to include("Created slices/main/admin/books/add.rb") end end end From 6a3c32ae8a20b83d4795ddb897fd80c8470fd24a Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Thu, 20 Jun 2024 15:09:54 -0600 Subject: [PATCH 16/56] Change examples --- lib/hanami/cli/commands/app/generate/operation.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/hanami/cli/commands/app/generate/operation.rb b/lib/hanami/cli/commands/app/generate/operation.rb index c12ece17..6c9a2ec3 100644 --- a/lib/hanami/cli/commands/app/generate/operation.rb +++ b/lib/hanami/cli/commands/app/generate/operation.rb @@ -18,8 +18,8 @@ class Operation < App::Command option :slice, required: false, desc: "Slice name" example [ - %(add_book (MyApp::Operations::AddBook)), - %(add_book --slice=admin (Admin::Operations::AddBook)), + %(books.add (MyApp::Books::Add)), + %(books.add --slice=admin (Admin::Books::Add)), ] attr_reader :generator private :generator From 8dc3de4c6df7b3c5a76efd7b62cc799cf1c826f0 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Fri, 21 Jun 2024 09:03:28 -0600 Subject: [PATCH 17/56] Switch to outputting directly, remove Files#recommend --- lib/hanami/cli/files.rb | 6 ------ lib/hanami/cli/generators/app/operation.rb | 11 +++++------ .../cli/commands/app/generate/operation_spec.rb | 12 +++++++++--- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/hanami/cli/files.rb b/lib/hanami/cli/files.rb index bceb568b..eb969e5c 100644 --- a/lib/hanami/cli/files.rb +++ b/lib/hanami/cli/files.rb @@ -42,12 +42,6 @@ def chdir(path, &blk) super end - # @since x.x.x - # @api private - def recommend(message) - out.puts(" Recommendation: #{message}") - end - private attr_reader :out diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index ec9b381d..afc92547 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -13,9 +13,10 @@ module App class Operation # @since x.x.x # @api private - def initialize(fs:, inflector:) + def initialize(fs:, inflector:, out: $stdout) @fs = fs @inflector = inflector + @out = out end # @since x.x.x @@ -32,9 +33,7 @@ def call(app, key, slice) private - attr_reader :fs - - attr_reader :inflector + attr_reader :fs, :inflector, :out def generate_for_slice(context, slice) slice_directory = fs.join("slices", slice) @@ -46,7 +45,7 @@ def generate_for_slice(context, slice) 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}/.") + out.puts(" Generating a top-level operation. To generate into a directory, add a namespace: `my_namespace.#{context.name}`") end end @@ -56,8 +55,8 @@ def generate_for_app(context) fs.write(fs.join(directory, "#{context.name}.rb"), t("nested_app_operation.erb", context)) else fs.mkdir(directory = fs.join("app")) + out.puts(" Generating 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)) - fs.recommend("Add a namespace to operation names, so they go into a folder within #{directory}/.") end end diff --git a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb index 09a571d8..9655ad71 100644 --- a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb +++ b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb @@ -8,7 +8,7 @@ let(:out) { StringIO.new } let(:fs) { Hanami::CLI::Files.new(memory: true, out: out) } let(:inflector) { Dry::Inflector.new } - let(:generator) { Hanami::CLI::Generators::App::Operation.new(fs: fs, inflector: inflector) } + let(:generator) { Hanami::CLI::Generators::App::Operation.new(fs: fs, inflector: inflector, out: out) } let(:app) { Hanami.app.namespace } let(:dir) { inflector.underscore(app) } @@ -33,7 +33,10 @@ def call expect(fs.read("app/add_book.rb")).to eq(operation_file) expect(output).to include("Created app/add_book.rb") - expect(output).to include(" Recommendation: Add a namespace to operation names, so they go into a folder within app/") + expect(output).to include( + " Generating a top-level operation. " \ + "To generate into a directory, add a namespace: `my_namespace.add_book`" + ) end it "generates a operation in a deep namespace with default separator" do @@ -99,7 +102,10 @@ def call expect(fs.read("slices/main/add_book.rb")).to eq(operation_file) expect(output).to include("Created slices/main/add_book.rb") - expect(output).to include(" Recommendation: Add a namespace to operation names, so they go into a folder within slices/main/") + expect(output).to include( + " Generating a top-level operation. " \ + "To generate into a directory, add a namespace: `my_namespace.add_book`" + ) end it "generates a operation in a nested namespace" do From 7059e7e47f5a9da638e3c278d581204cf5a3f42d Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Fri, 21 Jun 2024 09:38:14 -0600 Subject: [PATCH 18/56] Add Hanami::CLI::RubyFileGenerator --- lib/hanami/cli/ruby_file_generator.rb | 268 ++++++++ .../hanami/cli/ruby_file_generator_spec.rb | 614 ++++++++++++++++++ 2 files changed, 882 insertions(+) create mode 100644 lib/hanami/cli/ruby_file_generator.rb create mode 100644 spec/unit/hanami/cli/ruby_file_generator_spec.rb diff --git a/lib/hanami/cli/ruby_file_generator.rb b/lib/hanami/cli/ruby_file_generator.rb new file mode 100644 index 00000000..f90941f2 --- /dev/null +++ b/lib/hanami/cli/ruby_file_generator.rb @@ -0,0 +1,268 @@ +# frozen_string_literal: true + +require "prism" + +module Hanami + module CLI + class RubyFileGenerator + + class InvalidInstanceVariablesError < Error + def initialize + end + end + + class DuplicateInitializeMethodError < Error + def initialize + super("Initialize method cannot be defined if instance variables are present") + end + end + + class GeneratedUnparseableCodeError < Error + def initialize(source_code) + super( + <<~ERROR_MESSAGE + Sorry, the code we generated is not valid Ruby. + + Here's what we got: + + #{source_code} + + Please fix the errors and try again. + ERROR_MESSAGE + ) + end + end + INDENT = " " + + # rubocop:disable Metrics/ParameterLists + def initialize( + class_name: nil, + parent_class: nil, + modules: [], + requires: [], + relative_requires: [], + methods: {}, + includes: [], + top_contents: [], + magic_comments: {}, + ivars: [] + ) + @class_name = class_name + @parent_class = parent_class + @modules = modules + @requires = requires + @relative_requires = relative_requires + @methods = methods + @includes = includes + @top_contents = top_contents + @magic_comments = magic_comments.merge(frozen_string_literal).compact.sort + @ivar_names = parse_ivar_names!(ivars) + + raise DuplicateInitializeMethodError if methods.key?(:initialize) && ivars.any? + end + # rubocop:enable Metrics/ParameterLists + + def self.class(class_name, **) + new(class_name: class_name, **).to_s + end + + def self.module(*name, **) + module_names = if names.first.is_a?(Array) + names.first + else + names + end + + new(modules: module_names, class_name: nil, parent_class: nil, **).to_s + end + def to_s + definition = lines(modules).map { |line| "#{line}\n" }.join + + source_code = [file_directives, definition].flatten.join("\n") + + ensure_parseable!(source_code) + end + + + private + + attr_reader( + :class_name, + :parent_class, + :modules, + :requires, + :relative_requires, + :methods, + :includes, + :top_contents, + :magic_comments, + :ivar_names + ) + + def lines(remaining_modules) + this_module, *rest_modules = remaining_modules + if this_module + with_module_lines(this_module, lines(rest_modules)) + elsif class_name + class_lines + else + [] + end + end + + def with_module_lines(module_name, contents_lines) + [ + "module #{module_name}", + *contents_lines.map { |line| indent(line) }, + "end" + ] + end + + def file_directives + [magic_comments_lines, import_lines].compact + end + + def magic_comments_lines + lines = magic_comments + .map { |magic_key, magic_value| "# #{magic_key}: #{magic_value}" } + add_empty_line_if_any(lines) + end + + def frozen_string_literal + {frozen_string_literal: true} + end + + def import_lines + lines = [requires_lines, relative_requires_lines].flatten.compact + add_empty_line_if_any(lines) + end + + def requires_lines + requires.map do |require| + %(require "#{require}") + end + end + + def relative_requires_lines + relative_requires.map do |require| + %(require_relative "#{require}") + end + end + + def class_lines + if class_name + [ + class_definition, + *class_contents_lines, + "end" + ].compact + else + [] + end + end + + def includes_lines + if includes.any? + includes.map do |include| + "include #{include}" + end + end + end + + def top_contents_lines + if top_contents.any? + top_contents + end + end + + def class_contents_lines + line_groups = [ + includes_lines, + top_contents_lines, + initialize_lines, + *methods_lines, + *private_contents_lines + ].compact + add_empty_line_between_groups(line_groups).flatten.map { |line| indent(line) } + end + + def initialize_lines + if ivar_names.any? + [ + method_definition("initialize", ivar_names.map { |ivar| "#{ivar}:" }), + ivar_names.map { |ivar_name| indent("@#{ivar_name} = #{ivar_name}") }.flatten, + "end" + ] + end + end + + def private_contents_lines + if ivar_names.any? + [ + "private", + "attr_reader #{ivar_names.map { |ivar| ":#{ivar}" }.join(', ')}" + ] + end + end + + def methods_lines + methods.map do |method_name, args| + [method_definition(method_name, args), "end"] + end + end + + def class_definition + if parent_class + "class #{class_name} < #{parent_class}" + else + "class #{class_name}" + end + end + + def method_definition(method_name, args) + if args + "def #{method_name}(#{args.join(', ')})" + else + "def #{method_name}" + end + end + + def indent(line) + if line.strip.empty? + "" + else + INDENT + line + end + end + + def parse_ivar_names!(ivars) + if ivars.all? { |ivar| ivar.start_with?("@") } + ivars.map { |ivar| ivar.to_s.delete_prefix("@") } + else + raise InvalidInstanceVariablesError + end + end + + def ensure_parseable!(source_code) + parse_result = Prism.parse(source_code) + + if parse_result.success? + source_code + else + raise GeneratedUnparseableCodeError.new(source_code) + end + end + + def add_empty_line_if_any(lines) + if lines.any? + lines << "" + end + end + + def add_empty_line_between_groups(line_groups) + # We add an empty line after every group then remove the last one + line_groups.flat_map { |line_group| [line_group, ""] }[0...-1] + end + end + end +end diff --git a/spec/unit/hanami/cli/ruby_file_generator_spec.rb b/spec/unit/hanami/cli/ruby_file_generator_spec.rb new file mode 100644 index 00000000..b18ed8b7 --- /dev/null +++ b/spec/unit/hanami/cli/ruby_file_generator_spec.rb @@ -0,0 +1,614 @@ +# frozen_string_literal: true + +RSpec.describe Hanami::CLI::RubyFileGenerator do + describe "no methods" do + describe "top-level" do + it "generates class without parent class" do + expect( + described_class.class("Greeter") + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + class Greeter + end + OUTPUT + ) + ) + end + + it "generates class with parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + ).to_s + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + class Greeter < BaseService + end + OUTPUT + ) + ) + end + end + + describe "with single module" do + it "generates class without parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: %w[Services], + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module Services + class Greeter + end + end + OUTPUT + ) + ) + end + + it "generates class with parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + modules: %w[Services] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module Services + class Greeter < BaseService + end + end + OUTPUT + ) + ) + end + end + + describe "with two modules" do + it "generates class without parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: %w[Admin Services], + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module Admin + module Services + class Greeter + end + end + end + OUTPUT + ) + ) + end + + it "generates class with parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + modules: %w[Admin Services] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module Admin + module Services + class Greeter < BaseService + end + end + end + OUTPUT + ) + ) + end + end + + describe "with three modules" do + it "generates class without parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: %w[Internal Admin Services] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module Internal + module Admin + module Services + class Greeter + end + end + end + end + OUTPUT + ) + ) + end + + it "generates class with parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + modules: %w[Internal Admin Services] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module Internal + module Admin + module Services + class Greeter < BaseService + end + end + end + end + OUTPUT + ) + ) + end + end + end + + describe "with methods" do + describe "top-level" do + it "generates class without parent class and call method with no args" do + expect( + described_class.class("Greeter", methods: {call: nil}) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + class Greeter + def call + end + end + OUTPUT + ) + ) + end + + it "generates class with parent class and call method with 1 arg" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + methods: {call: ["args"]} + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + class Greeter < BaseService + def call(args) + end + end + OUTPUT + ) + ) + end + end + + describe "with single module" do + it "generates class without parent class and call methods with 2 args" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: %w[Services], + methods: {call: %w[request response]} + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module Services + class Greeter + def call(request, response) + end + end + end + OUTPUT + ) + ) + end + + it "generates class with parent class and call method with required keyword args" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + modules: %w[Services], + methods: {call: %w[request: response:]} + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module Services + class Greeter < BaseService + def call(request:, response:) + end + end + end + OUTPUT + ) + ) + end + end + + describe "with two modules" do + it "generates class without parent class and call method with mix of args" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: %w[Admin Services], + methods: {call: ["env", "request:", "response:", "context: nil"]} + ).to_s + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module Admin + module Services + class Greeter + def call(env, request:, response:, context: nil) + end + end + end + end + OUTPUT + ) + ) + end + + it "generates class with parent class and two methods" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + modules: %w[Admin Services], + methods: {initialize: ["context"], call: ["args"]} + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module Admin + module Services + class Greeter < BaseService + def initialize(context) + end + + def call(args) + end + end + end + end + OUTPUT + ) + ) + end + end + + describe "with three modules" do + it "generates class without parent class, with ivars and method" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: %w[Internal Admin Services], + ivars: [:@name, :@birthdate], + methods: {call: [:env]} + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module Internal + module Admin + module Services + class Greeter + def initialize(name:, birthdate:) + @name = name + @birthdate = birthdate + end + + def call(env) + end + + private + + attr_reader :name, :birthdate + end + end + end + end + OUTPUT + ) + ) + end + + it "raises error when ivars don't lead with @" do + expect { + described_class.class("Greeter", ivars: [:name]) + }.to(raise_error(Hanami::CLI::RubyFileGenerator::InvalidInstanceVariablesError)) + end + + it "raises error when 'initialize' method is specified and ivars are present" do + expect { + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + ivars: [:@name], + methods: {initialize: nil} + ) + }.to(raise_error(Hanami::CLI::RubyFileGenerator::DuplicateInitializeMethodError)) + end + + it "generates class with parent class, and requires" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + modules: %w[Internal Admin Services], + requires: ["roobi/fake"] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + require "roobi/fake" + + module Internal + module Admin + module Services + class Greeter < BaseService + end + end + end + end + OUTPUT + ) + ) + end + end + + describe "with includes" do + it "generates class with includes" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + includes: ["Enumerable", %(Import["external.api"])] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + class Greeter + include Enumerable + include Import["external.api"] + end + OUTPUT + ) + ) + end + + it "generates class with includes and ivars" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + includes: ["Enumerable", %(Import["external.api"])], + ivars: [:@name] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + class Greeter + include Enumerable + include Import["external.api"] + + def initialize(name:) + @name = name + end + + private + + attr_reader :name + end + OUTPUT + ) + ) + end + + it "generates class with includes and one method" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + includes: ["Enumerable", %(Import["external.api"])], + methods: {call: ["name"]} + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + class Greeter + include Enumerable + include Import["external.api"] + + def call(name) + end + end + OUTPUT + ) + ) + end + end + + describe "with inline syntax name for parent, module, class" do + it "generates class with inline-syntax" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Services::Greeter", + parent_class: "Internal::BaseService", + modules: ["Internal::Admin"] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module Internal::Admin + class Services::Greeter < Internal::BaseService + end + end + OUTPUT + ) + ) + end + end + + describe "with magic comment" do + it "generates class with custom magic comment" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: ["Internal"], + magic_comments: {value: true} + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + # value: true + + module Internal + class Greeter + end + end + OUTPUT + ) + ) + end + end + + describe "with top contents" do + it "generates simple class with only top contents as comment" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Foo", + top_contents: ["# code goes here"] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + class Foo + # code goes here + end + OUTPUT + ) + ) + end + + it "generates class with top contents in correct spot" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + includes: ["Validatable"], + ivars: [:@name], + top_contents: ["before_call :validate"] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + class Greeter + include Validatable + + before_call :validate + + def initialize(name:) + @name = name + end + + private + + attr_reader :name + end + OUTPUT + ) + ) + end + end + end + + it "generates class with sorted custom magic comments, including frozen_string_literal" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: ["Internal"], + magic_comments: {abc: 123, value: true} + ) + ).to( + eq( + <<~OUTPUT + # abc: 123 + # frozen_string_literal: true + # value: true + + module Internal + class Greeter + end + end + OUTPUT + ) + ) + end + + it "fails to generate unparseable ruby code" do + expect { described_class.class("%%Greeter") }.to( + raise_error(Hanami::CLI::RubyFileGenerator::GeneratedUnparseableCodeError) + ) + end +end From 0dfdf7d9194d8c83ac636161b29eac2bd0204b87 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Sat, 22 Jun 2024 09:34:14 -0600 Subject: [PATCH 19/56] x.x.x => 2.2.0 --- .../cli/commands/app/generate/operation.rb | 6 +++--- lib/hanami/cli/generators/app/operation.rb | 6 +++--- .../cli/generators/app/operation_context.rb | 20 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/hanami/cli/commands/app/generate/operation.rb b/lib/hanami/cli/commands/app/generate/operation.rb index 6c9a2ec3..ad4fb374 100644 --- a/lib/hanami/cli/commands/app/generate/operation.rb +++ b/lib/hanami/cli/commands/app/generate/operation.rb @@ -11,7 +11,7 @@ module CLI module Commands module App module Generate - # @since x.x.x + # @since 2.2.0 # @api private class Operation < App::Command argument :name, required: true, desc: "Operation name" @@ -24,7 +24,7 @@ class Operation < App::Command attr_reader :generator private :generator - # @since x.x.x + # @since 2.2.0 # @api private def initialize( fs:, inflector:, @@ -35,7 +35,7 @@ def initialize( @generator = generator end - # @since x.x.x + # @since 2.2.0 # @api private def call(name:, slice: nil, **) slice = inflector.underscore(Shellwords.shellescape(slice)) if slice diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index afc92547..67c6e035 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -8,10 +8,10 @@ module Hanami module CLI module Generators module App - # @since x.x.x + # @since 2.2.0 # @api private class Operation - # @since x.x.x + # @since 2.2.0 # @api private def initialize(fs:, inflector:, out: $stdout) @fs = fs @@ -19,7 +19,7 @@ def initialize(fs:, inflector:, out: $stdout) @out = out end - # @since x.x.x + # @since 2.2.0 # @api private def call(app, key, slice) context = OperationContext.new(inflector, app, slice, key) diff --git a/lib/hanami/cli/generators/app/operation_context.rb b/lib/hanami/cli/generators/app/operation_context.rb index 6e17bdfb..03ac07a2 100644 --- a/lib/hanami/cli/generators/app/operation_context.rb +++ b/lib/hanami/cli/generators/app/operation_context.rb @@ -6,10 +6,10 @@ module Hanami module CLI module Generators - # @since x.x.x + # @since 2.2.0 # @api private module App - # @since x.x.x + # @since 2.2.0 # @api private class OperationContext < SliceContext # TODO: move these constants somewhere that will let us reuse them @@ -25,37 +25,37 @@ class OperationContext < SliceContext OFFSET = INDENTATION private_constant :OFFSET - # @since x.x.x + # @since 2.2.0 # @api private attr_reader :key - # @since x.x.x + # @since 2.2.0 # @api private def initialize(inflector, app, slice, key) @key = key super(inflector, app, slice, nil) end - # @since x.x.x + # @since 2.2.0 # @api private def namespaces @namespaces ||= key.split(KEY_SEPARATOR)[..-2] end - # @since x.x.x + # @since 2.2.0 # @api private def name @name ||= key.split(KEY_SEPARATOR)[-1] end # @api private - # @since x.x.x + # @since 2.2.0 # @api private def camelized_name inflector.camelize(name) end - # @since x.x.x + # @since 2.2.0 # @api private def module_namespace_declaration namespaces.each_with_index.map { |token, i| @@ -63,7 +63,7 @@ def module_namespace_declaration }.join($/) end - # @since x.x.x + # @since 2.2.0 # @api private def module_namespace_end namespaces.each_with_index.map { |_, i| @@ -71,7 +71,7 @@ def module_namespace_end }.reverse.join($/) end - # @since x.x.x + # @since 2.2.0 # @api private def module_namespace_offset "#{OFFSET}#{INDENTATION * namespaces.count}" From 8f90b33f26ff18ab44434854705db82bd13f2474 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Sat, 22 Jun 2024 09:34:49 -0600 Subject: [PATCH 20/56] x.x.x => 2.2.0 --- .../cli/commands/app/generate/operation.rb | 6 +++--- lib/hanami/cli/generators/app/operation.rb | 6 +++--- .../cli/generators/app/operation_context.rb | 20 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/hanami/cli/commands/app/generate/operation.rb b/lib/hanami/cli/commands/app/generate/operation.rb index 6c9a2ec3..ad4fb374 100644 --- a/lib/hanami/cli/commands/app/generate/operation.rb +++ b/lib/hanami/cli/commands/app/generate/operation.rb @@ -11,7 +11,7 @@ module CLI module Commands module App module Generate - # @since x.x.x + # @since 2.2.0 # @api private class Operation < App::Command argument :name, required: true, desc: "Operation name" @@ -24,7 +24,7 @@ class Operation < App::Command attr_reader :generator private :generator - # @since x.x.x + # @since 2.2.0 # @api private def initialize( fs:, inflector:, @@ -35,7 +35,7 @@ def initialize( @generator = generator end - # @since x.x.x + # @since 2.2.0 # @api private def call(name:, slice: nil, **) slice = inflector.underscore(Shellwords.shellescape(slice)) if slice diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index afc92547..67c6e035 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -8,10 +8,10 @@ module Hanami module CLI module Generators module App - # @since x.x.x + # @since 2.2.0 # @api private class Operation - # @since x.x.x + # @since 2.2.0 # @api private def initialize(fs:, inflector:, out: $stdout) @fs = fs @@ -19,7 +19,7 @@ def initialize(fs:, inflector:, out: $stdout) @out = out end - # @since x.x.x + # @since 2.2.0 # @api private def call(app, key, slice) context = OperationContext.new(inflector, app, slice, key) diff --git a/lib/hanami/cli/generators/app/operation_context.rb b/lib/hanami/cli/generators/app/operation_context.rb index 6e17bdfb..03ac07a2 100644 --- a/lib/hanami/cli/generators/app/operation_context.rb +++ b/lib/hanami/cli/generators/app/operation_context.rb @@ -6,10 +6,10 @@ module Hanami module CLI module Generators - # @since x.x.x + # @since 2.2.0 # @api private module App - # @since x.x.x + # @since 2.2.0 # @api private class OperationContext < SliceContext # TODO: move these constants somewhere that will let us reuse them @@ -25,37 +25,37 @@ class OperationContext < SliceContext OFFSET = INDENTATION private_constant :OFFSET - # @since x.x.x + # @since 2.2.0 # @api private attr_reader :key - # @since x.x.x + # @since 2.2.0 # @api private def initialize(inflector, app, slice, key) @key = key super(inflector, app, slice, nil) end - # @since x.x.x + # @since 2.2.0 # @api private def namespaces @namespaces ||= key.split(KEY_SEPARATOR)[..-2] end - # @since x.x.x + # @since 2.2.0 # @api private def name @name ||= key.split(KEY_SEPARATOR)[-1] end # @api private - # @since x.x.x + # @since 2.2.0 # @api private def camelized_name inflector.camelize(name) end - # @since x.x.x + # @since 2.2.0 # @api private def module_namespace_declaration namespaces.each_with_index.map { |token, i| @@ -63,7 +63,7 @@ def module_namespace_declaration }.join($/) end - # @since x.x.x + # @since 2.2.0 # @api private def module_namespace_end namespaces.each_with_index.map { |_, i| @@ -71,7 +71,7 @@ def module_namespace_end }.reverse.join($/) end - # @since x.x.x + # @since 2.2.0 # @api private def module_namespace_offset "#{OFFSET}#{INDENTATION * namespaces.count}" From 8e62aa352ac382dde6ced0cd360c6528f80408fb Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Sat, 22 Jun 2024 10:04:49 -0600 Subject: [PATCH 21/56] Include Dry::Monads[:result] in base Action --- lib/hanami/cli/generators/gem/app/action.erb | 1 + spec/unit/hanami/cli/commands/gem/new_spec.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/hanami/cli/generators/gem/app/action.erb b/lib/hanami/cli/generators/gem/app/action.erb index c573be65..56eab2e4 100644 --- a/lib/hanami/cli/generators/gem/app/action.erb +++ b/lib/hanami/cli/generators/gem/app/action.erb @@ -5,5 +5,6 @@ require "hanami/action" module <%= camelized_app_name %> class Action < Hanami::Action + include Dry::Monads[:result] end end diff --git a/spec/unit/hanami/cli/commands/gem/new_spec.rb b/spec/unit/hanami/cli/commands/gem/new_spec.rb index e476107c..c535aaa2 100644 --- a/spec/unit/hanami/cli/commands/gem/new_spec.rb +++ b/spec/unit/hanami/cli/commands/gem/new_spec.rb @@ -341,6 +341,7 @@ class Routes < Hanami::Routes module #{inflector.camelize(app)} class Action < Hanami::Action + include Dry::Monads[:result] end end EXPECTED From f0e09944a5b6a541b5a455fdc2e4793f95b41374 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 12:27:06 -0600 Subject: [PATCH 22/56] Add .module tests --- lib/hanami/cli/ruby_file_generator.rb | 3 +- .../hanami/cli/ruby_file_generator_spec.rb | 1031 +++++++++-------- 2 files changed, 548 insertions(+), 486 deletions(-) diff --git a/lib/hanami/cli/ruby_file_generator.rb b/lib/hanami/cli/ruby_file_generator.rb index f90941f2..d0fe77f6 100644 --- a/lib/hanami/cli/ruby_file_generator.rb +++ b/lib/hanami/cli/ruby_file_generator.rb @@ -66,7 +66,7 @@ def self.class(class_name, **) new(class_name: class_name, **).to_s end - def self.module(*name, **) + def self.module(*names, **) module_names = if names.first.is_a?(Array) names.first else @@ -75,6 +75,7 @@ def self.module(*name, **) new(modules: module_names, class_name: nil, parent_class: nil, **).to_s end + def to_s definition = lines(modules).map { |line| "#{line}\n" }.join diff --git a/spec/unit/hanami/cli/ruby_file_generator_spec.rb b/spec/unit/hanami/cli/ruby_file_generator_spec.rb index b18ed8b7..a9c32d32 100644 --- a/spec/unit/hanami/cli/ruby_file_generator_spec.rb +++ b/spec/unit/hanami/cli/ruby_file_generator_spec.rb @@ -1,614 +1,675 @@ # frozen_string_literal: true RSpec.describe Hanami::CLI::RubyFileGenerator do - describe "no methods" do - describe "top-level" do - it "generates class without parent class" do - expect( - described_class.class("Greeter") - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true + describe ".class" do + describe "no methods" do + describe "top-level" do + it "generates class without parent class" do + expect( + described_class.class("Greeter") + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true - class Greeter - end - OUTPUT - ) - ) - end - - it "generates class with parent class" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - parent_class: "BaseService", - ).to_s - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - class Greeter < BaseService - end - OUTPUT - ) - ) - end - end - - describe "with single module" do - it "generates class without parent class" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - modules: %w[Services], - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - module Services class Greeter end - end - OUTPUT - ) - ) - end + OUTPUT + ) + ) + end + + it "generates class with parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + ).to_s + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true - it "generates class with parent class" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - parent_class: "BaseService", - modules: %w[Services] - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - module Services class Greeter < BaseService end - end - OUTPUT + OUTPUT + ) ) - ) + end end - end - describe "with two modules" do - it "generates class without parent class" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - modules: %w[Admin Services], - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true + describe "with single module" do + it "generates class without parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: %w[Services], + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true - module Admin module Services class Greeter end end - end - OUTPUT - ) - ) - end - - it "generates class with parent class" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - parent_class: "BaseService", - modules: %w[Admin Services] - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true + OUTPUT + ) + ) + end + + it "generates class with parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + modules: %w[Services] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true - module Admin module Services class Greeter < BaseService end end - end - OUTPUT + OUTPUT + ) ) - ) + end end - end - describe "with three modules" do - it "generates class without parent class" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - modules: %w[Internal Admin Services] - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true + describe "with two modules" do + it "generates class without parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: %w[Admin Services], + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true - module Internal module Admin module Services class Greeter end end end - end - OUTPUT - ) - ) - end - - it "generates class with parent class" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - parent_class: "BaseService", - modules: %w[Internal Admin Services] - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true + OUTPUT + ) + ) + end + + it "generates class with parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + modules: %w[Admin Services] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true - module Internal module Admin module Services class Greeter < BaseService end end end - end - OUTPUT + OUTPUT + ) ) - ) + end end - end - end - - describe "with methods" do - describe "top-level" do - it "generates class without parent class and call method with no args" do - expect( - described_class.class("Greeter", methods: {call: nil}) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - class Greeter - def call + describe "with three modules" do + it "generates class without parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: %w[Internal Admin Services] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module Internal + module Admin + module Services + class Greeter + end + end + end end - end - OUTPUT - ) - ) - end - - it "generates class with parent class and call method with 1 arg" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - parent_class: "BaseService", - methods: {call: ["args"]} - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - class Greeter < BaseService - def call(args) + OUTPUT + ) + ) + end + + it "generates class with parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + modules: %w[Internal Admin Services] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module Internal + module Admin + module Services + class Greeter < BaseService + end + end + end end - end - OUTPUT + OUTPUT + ) ) - ) + end end end - describe "with single module" do - it "generates class without parent class and call methods with 2 args" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - modules: %w[Services], - methods: {call: %w[request response]} - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true + describe "with methods" do + describe "top-level" do + it "generates class without parent class and call method with no args" do + expect( + described_class.class("Greeter", methods: {call: nil}) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true - module Services class Greeter - def call(request, response) + def call end end - end - OUTPUT - ) - ) - end + OUTPUT + ) + ) + end + + it "generates class with parent class and call method with 1 arg" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + methods: {call: ["args"]} + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true - it "generates class with parent class and call method with required keyword args" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - parent_class: "BaseService", - modules: %w[Services], - methods: {call: %w[request: response:]} - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - module Services class Greeter < BaseService - def call(request:, response:) + def call(args) end end - end - OUTPUT + OUTPUT + ) ) - ) + end end - end - describe "with two modules" do - it "generates class without parent class and call method with mix of args" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - modules: %w[Admin Services], - methods: {call: ["env", "request:", "response:", "context: nil"]} - ).to_s - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - module Admin + describe "with single module" do + it "generates class without parent class and call methods with 2 args" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: %w[Services], + methods: {call: %w[request response]} + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + module Services class Greeter - def call(env, request:, response:, context: nil) + def call(request, response) end end end - end - OUTPUT - ) - ) - end - - it "generates class with parent class and two methods" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - parent_class: "BaseService", - modules: %w[Admin Services], - methods: {initialize: ["context"], call: ["args"]} - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true + OUTPUT + ) + ) + end + + it "generates class with parent class and call method with required keyword args" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + modules: %w[Services], + methods: {call: %w[request: response:]} + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true - module Admin module Services class Greeter < BaseService - def initialize(context) - end - - def call(args) + def call(request:, response:) end end end - end - OUTPUT + OUTPUT + ) ) - ) + end end - end - describe "with three modules" do - it "generates class without parent class, with ivars and method" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - modules: %w[Internal Admin Services], - ivars: [:@name, :@birthdate], - methods: {call: [:env]} - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true + describe "with two modules" do + it "generates class without parent class and call method with mix of args" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: %w[Admin Services], + methods: {call: ["env", "request:", "response:", "context: nil"]} + ).to_s + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true - module Internal module Admin module Services class Greeter - def initialize(name:, birthdate:) - @name = name - @birthdate = birthdate + def call(env, request:, response:, context: nil) end - - def call(env) - end - - private - - attr_reader :name, :birthdate end end end - end - OUTPUT - ) - ) - end - - it "raises error when ivars don't lead with @" do - expect { - described_class.class("Greeter", ivars: [:name]) - }.to(raise_error(Hanami::CLI::RubyFileGenerator::InvalidInstanceVariablesError)) - end - - it "raises error when 'initialize' method is specified and ivars are present" do - expect { - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - ivars: [:@name], - methods: {initialize: nil} - ) - }.to(raise_error(Hanami::CLI::RubyFileGenerator::DuplicateInitializeMethodError)) - end - - it "generates class with parent class, and requires" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - parent_class: "BaseService", - modules: %w[Internal Admin Services], - requires: ["roobi/fake"] - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - require "roobi/fake" + OUTPUT + ) + ) + end + + it "generates class with parent class and two methods" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + modules: %w[Admin Services], + methods: {initialize: ["context"], call: ["args"]} + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true - module Internal module Admin module Services class Greeter < BaseService + def initialize(context) + end + + def call(args) + end end end end - end - OUTPUT + OUTPUT + ) ) - ) + end end - end - - describe "with includes" do - it "generates class with includes" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - includes: ["Enumerable", %(Import["external.api"])] - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - class Greeter - include Enumerable - include Import["external.api"] - end - OUTPUT + describe "with three modules" do + it "generates class without parent class, with ivars and method" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: %w[Internal Admin Services], + ivars: [:@name, :@birthdate], + methods: {call: [:env]} + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module Internal + module Admin + module Services + class Greeter + def initialize(name:, birthdate:) + @name = name + @birthdate = birthdate + end + + def call(env) + end + + private + + attr_reader :name, :birthdate + end + end + end + end + OUTPUT + ) + ) + end + + it "raises error when ivars don't lead with @" do + expect { + described_class.class("Greeter", ivars: [:name]) + }.to(raise_error(Hanami::CLI::RubyFileGenerator::InvalidInstanceVariablesError)) + end + + it "raises error when 'initialize' method is specified and ivars are present" do + expect { + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + ivars: [:@name], + methods: {initialize: nil} + ) + }.to(raise_error(Hanami::CLI::RubyFileGenerator::DuplicateInitializeMethodError)) + end + + it "generates class with parent class, and requires" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + modules: %w[Internal Admin Services], + requires: ["roobi/fake"] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + require "roobi/fake" + + module Internal + module Admin + module Services + class Greeter < BaseService + end + end + end + end + OUTPUT + ) ) - ) + end end - it "generates class with includes and ivars" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - includes: ["Enumerable", %(Import["external.api"])], - ivars: [:@name] - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - class Greeter - include Enumerable - include Import["external.api"] + describe "with includes" do + it "generates class with includes" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + includes: ["Enumerable", %(Import["external.api"])] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true - def initialize(name:) - @name = name + class Greeter + include Enumerable + include Import["external.api"] end + OUTPUT + ) + ) + end + + it "generates class with includes and ivars" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + includes: ["Enumerable", %(Import["external.api"])], + ivars: [:@name] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true - private - - attr_reader :name - end - OUTPUT - ) - ) - end + class Greeter + include Enumerable + include Import["external.api"] - it "generates class with includes and one method" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - includes: ["Enumerable", %(Import["external.api"])], - methods: {call: ["name"]} - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true + def initialize(name:) + @name = name + end - class Greeter - include Enumerable - include Import["external.api"] + private - def call(name) + attr_reader :name end - end - OUTPUT - ) - ) - end - end + OUTPUT + ) + ) + end + + it "generates class with includes and one method" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + includes: ["Enumerable", %(Import["external.api"])], + methods: {call: ["name"]} + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true - describe "with inline syntax name for parent, module, class" do - it "generates class with inline-syntax" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Services::Greeter", - parent_class: "Internal::BaseService", - modules: ["Internal::Admin"] - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true + class Greeter + include Enumerable + include Import["external.api"] - module Internal::Admin - class Services::Greeter < Internal::BaseService + def call(name) + end end - end - OUTPUT + OUTPUT + ) ) - ) + end end - end - describe "with magic comment" do - it "generates class with custom magic comment" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - modules: ["Internal"], - magic_comments: {value: true} + describe "with inline syntax name for parent, module, class" do + it "generates class with inline-syntax" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Services::Greeter", + parent_class: "Internal::BaseService", + modules: ["Internal::Admin"] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module Internal::Admin + class Services::Greeter < Internal::BaseService + end + end + OUTPUT + ) ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - # value: true + end + end - module Internal - class Greeter + describe "with magic comment" do + it "generates class with custom magic comment" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: ["Internal"], + magic_comments: {value: true} + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + # value: true + + module Internal + class Greeter + end end - end - OUTPUT + OUTPUT + ) ) - ) + end end - end - describe "with top contents" do - it "generates simple class with only top contents as comment" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Foo", - top_contents: ["# code goes here"] - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true + describe "with top contents" do + it "generates simple class with only top contents as comment" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Foo", + top_contents: ["# code goes here"] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + class Foo + # code goes here + end + OUTPUT + ) + ) + end + + it "generates class with top contents in correct spot" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + includes: ["Validatable"], + ivars: [:@name], + top_contents: ["before_call :validate"] + ) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true - class Foo - # code goes here - end - OUTPUT - ) - ) - end + class Greeter + include Validatable - it "generates class with top contents in correct spot" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - includes: ["Validatable"], - ivars: [:@name], - top_contents: ["before_call :validate"] - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true + before_call :validate - class Greeter - include Validatable + def initialize(name:) + @name = name + end - before_call :validate + private - def initialize(name:) - @name = name + attr_reader :name end + OUTPUT + ) + ) + end + end + end - private - - attr_reader :name + it "generates class with sorted custom magic comments, including frozen_string_literal" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: ["Internal"], + magic_comments: {abc: 123, value: true} + ) + ).to( + eq( + <<~OUTPUT + # abc: 123 + # frozen_string_literal: true + # value: true + + module Internal + class Greeter end - OUTPUT - ) + end + OUTPUT ) - end + ) end - end - it "generates class with sorted custom magic comments, including frozen_string_literal" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - modules: ["Internal"], - magic_comments: {abc: 123, value: true} + it "fails to generate unparseable ruby code" do + expect { described_class.class("%%Greeter") }.to( + raise_error(Hanami::CLI::RubyFileGenerator::GeneratedUnparseableCodeError) ) - ).to( - eq( - <<~OUTPUT - # abc: 123 - # frozen_string_literal: true - # value: true - - module Internal - class Greeter + end + + describe ".module" do + describe "no methods" do + describe "without frozen_string_literal" do + describe "top-level" do + it "generates module by itself" do + expect( + described_class.module("Greetable") + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module Greetable + end + OUTPUT + ) + ) end - end - OUTPUT - ) - ) - end - it "fails to generate unparseable ruby code" do - expect { described_class.class("%%Greeter") }.to( - raise_error(Hanami::CLI::RubyFileGenerator::GeneratedUnparseableCodeError) - ) + it "generates modules nested in a module, from arrray" do + expect( + described_class.module(%w[External Greetable]) + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module External + module Greetable + end + end + OUTPUT + ) + ) + end + + it "generates modules nested in a module, from list" do + expect( + described_class.module("Admin", "External", "Greetable") + ).to( + eq( + <<~OUTPUT + # frozen_string_literal: true + + module Admin + module External + module Greetable + end + end + end + OUTPUT + ) + ) + end + end + end + end + end end end From ed47e5e88551ec158d2951ade800ec550cd0c5b0 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 12:50:08 -0600 Subject: [PATCH 23/56] Convert top-level app operation to use RubyFileGenerator --- lib/hanami/cli/generators/app/operation.rb | 22 ++++++++++++++++++- .../app/operation/top_level_app_operation.erb | 8 ------- 2 files changed, 21 insertions(+), 9 deletions(-) delete mode 100644 lib/hanami/cli/generators/app/operation/top_level_app_operation.erb diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index 67c6e035..374a87ca 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -23,6 +23,8 @@ def initialize(fs:, inflector:, out: $stdout) # @api private def call(app, key, slice) context = OperationContext.new(inflector, app, slice, key) + @app = app + @key = key if slice generate_for_slice(context, slice) @@ -35,6 +37,18 @@ def call(app, key, slice) attr_reader :fs, :inflector, :out + def camelized_app_name + inflector.camelize(@app).gsub(/[^\p{Alnum}]/, "") + end + + def camelized_operation_name + inflector.camelize(@key).gsub(/[^\p{Alnum}]/, "") + end + + def camelized_parent_operation_name + [camelized_app_name, "Operation"].join("::") + end + def generate_for_slice(context, slice) slice_directory = fs.join("slices", slice) raise MissingSliceError.new(slice) unless fs.directory?(slice_directory) @@ -54,9 +68,15 @@ def generate_for_app(context) fs.mkdir(directory = fs.join("app", context.namespaces)) fs.write(fs.join(directory, "#{context.name}.rb"), t("nested_app_operation.erb", context)) else + class_definition = RubyFileGenerator.class( + camelized_operation_name, + parent_class: camelized_parent_operation_name, + modules: [camelized_app_name], + methods: {call: nil} + ) fs.mkdir(directory = fs.join("app")) out.puts(" Generating 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)) + fs.write(fs.join(directory, "#{context.name}.rb"), class_definition) end end diff --git a/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb b/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb deleted file mode 100644 index 0ce59f47..00000000 --- a/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb +++ /dev/null @@ -1,8 +0,0 @@ -# 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 From 4769bd697dd2427e930dead3fe2628297560e422 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 12:54:41 -0600 Subject: [PATCH 24/56] Convert nested app operation to use RubyFileGenerator --- lib/hanami/cli/generators/app/operation.rb | 42 +++++++++++++------ .../app/operation/nested_app_operation.erb | 10 ----- 2 files changed, 29 insertions(+), 23 deletions(-) delete mode 100644 lib/hanami/cli/generators/app/operation/nested_app_operation.erb diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index 374a87ca..910b8cad 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -25,6 +25,7 @@ def call(app, key, slice) context = OperationContext.new(inflector, app, slice, key) @app = app @key = key + @namespaces = context.namespaces if slice generate_for_slice(context, slice) @@ -42,13 +43,30 @@ def camelized_app_name end def camelized_operation_name - inflector.camelize(@key).gsub(/[^\p{Alnum}]/, "") + key = @key.split(".")[-1] + inflector.camelize(key).gsub(/[^\p{Alnum}]/, "") end def camelized_parent_operation_name [camelized_app_name, "Operation"].join("::") end + def camelized_modules + if @namespaces.any? + [camelized_app_name].push(@namespaces.map { inflector.camelize(_1) }).flatten + else + [camelized_app_name] + end + end + + def directory + if @namespaces.any? + fs.join("app", @namespaces) + else + fs.join("app") + end + end + def generate_for_slice(context, slice) slice_directory = fs.join("slices", slice) raise MissingSliceError.new(slice) unless fs.directory?(slice_directory) @@ -64,20 +82,18 @@ def generate_for_slice(context, slice) 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 - class_definition = RubyFileGenerator.class( - camelized_operation_name, - parent_class: camelized_parent_operation_name, - modules: [camelized_app_name], - methods: {call: nil} - ) - fs.mkdir(directory = fs.join("app")) + class_definition = RubyFileGenerator.class( + camelized_operation_name, + parent_class: camelized_parent_operation_name, + modules: camelized_modules, + methods: {call: nil} + ) + + fs.mkdir(directory) + if context.namespaces.none? out.puts(" Generating a top-level operation. To generate into a directory, add a namespace: `my_namespace.#{context.name}`") - fs.write(fs.join(directory, "#{context.name}.rb"), class_definition) end + fs.write(fs.join(directory, "#{context.name}.rb"), class_definition) end def template(path, context) diff --git a/lib/hanami/cli/generators/app/operation/nested_app_operation.erb b/lib/hanami/cli/generators/app/operation/nested_app_operation.erb deleted file mode 100644 index f0b7a131..00000000 --- a/lib/hanami/cli/generators/app/operation/nested_app_operation.erb +++ /dev/null @@ -1,10 +0,0 @@ -# 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 From 8e5c5752a190b048a3c38f5d08f2d9bbcdbb1f31 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 13:17:29 -0600 Subject: [PATCH 25/56] Support slash separators --- lib/hanami/cli/generators/app/operation.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index 910b8cad..dab1467d 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -11,6 +11,7 @@ module App # @since 2.2.0 # @api private class Operation + KEY_SEPARATOR = %r{\.|/} # @since 2.2.0 # @api private def initialize(fs:, inflector:, out: $stdout) @@ -43,7 +44,7 @@ def camelized_app_name end def camelized_operation_name - key = @key.split(".")[-1] + key = @key.split(KEY_SEPARATOR)[-1] inflector.camelize(key).gsub(/[^\p{Alnum}]/, "") end From 8effa30aa053441f45b98bebdec55317e352247b Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 13:23:28 -0600 Subject: [PATCH 26/56] Convert top-level slice operation to use RubyFileGenerator --- lib/hanami/cli/generators/app/operation.rb | 67 +++++++++---------- .../operation/top_level_slice_operation.erb | 8 --- 2 files changed, 30 insertions(+), 45 deletions(-) delete mode 100644 lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index dab1467d..30d9ce5d 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -28,11 +28,7 @@ def call(app, key, slice) @key = key @namespaces = context.namespaces - if slice - generate_for_slice(context, slice) - else - generate_for_app(context) - end + generate_file(context, slice) end private @@ -48,64 +44,61 @@ def camelized_operation_name inflector.camelize(key).gsub(/[^\p{Alnum}]/, "") end - def camelized_parent_operation_name - [camelized_app_name, "Operation"].join("::") + def camelized_parent_operation_name(slice = nil) + [highest_level_module(slice), "Operation"].join("::") end - def camelized_modules - if @namespaces.any? - [camelized_app_name].push(@namespaces.map { inflector.camelize(_1) }).flatten + def highest_level_module(slice) + if slice + inflector.camelize(slice).gsub(/[^\p{Alnum}]/, "") else - [camelized_app_name] + camelized_app_name end end - def directory + def camelized_modules(slice = nil) if @namespaces.any? - fs.join("app", @namespaces) + [highest_level_module(slice)].push(@namespaces.map { inflector.camelize(_1) }).flatten else - fs.join("app") + [highest_level_module(slice)] end end - def generate_for_slice(context, slice) - slice_directory = fs.join("slices", slice) - raise MissingSliceError.new(slice) unless fs.directory?(slice_directory) + def directory(slice = nil) + base = if slice + fs.join("slices", slice) + else + fs.join("app") + end - 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)) + if @namespaces.any? + fs.join(base, @namespaces) 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(" Generating a top-level operation. To generate into a directory, add a namespace: `my_namespace.#{context.name}`") + fs.join(base) end end - def generate_for_app(context) + def generate_file(context, slice = nil) + if slice + slice_directory = fs.join("slices", slice) + raise MissingSliceError.new(slice) unless fs.directory?(slice_directory) + end + class_definition = RubyFileGenerator.class( camelized_operation_name, - parent_class: camelized_parent_operation_name, - modules: camelized_modules, + parent_class: camelized_parent_operation_name(slice), + modules: camelized_modules(slice), methods: {call: nil} ) - fs.mkdir(directory) + fs.mkdir(directory(slice)) + if context.namespaces.none? out.puts(" Generating a top-level operation. To generate into a directory, add a namespace: `my_namespace.#{context.name}`") end - fs.write(fs.join(directory, "#{context.name}.rb"), class_definition) - end - - def template(path, context) - require "erb" - ERB.new( - File.read(__dir__ + "/operation/#{path}") - ).result(context.ctx) + fs.write(fs.join(directory(slice), "#{context.name}.rb"), class_definition) end - - alias_method :t, :template end end end diff --git a/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb b/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb deleted file mode 100644 index 240448ab..00000000 --- a/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb +++ /dev/null @@ -1,8 +0,0 @@ -# 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 From 06f27508753433a541ef7f2f3a550b78590d7513 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 13:48:27 -0600 Subject: [PATCH 27/56] Remove OperationContext --- lib/hanami/cli/generators/app/operation.rb | 39 +++++---- .../cli/generators/app/operation_context.rb | 83 ------------------- 2 files changed, 24 insertions(+), 98 deletions(-) delete mode 100644 lib/hanami/cli/generators/app/operation_context.rb diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index 30d9ce5d..feb04853 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -23,12 +23,11 @@ def initialize(fs:, inflector:, out: $stdout) # @since 2.2.0 # @api private def call(app, key, slice) - context = OperationContext.new(inflector, app, slice, key) @app = app @key = key - @namespaces = context.namespaces + @namespaces = key.split(KEY_SEPARATOR)[..-2] - generate_file(context, slice) + generate_file(slice) end private @@ -39,9 +38,12 @@ def camelized_app_name inflector.camelize(@app).gsub(/[^\p{Alnum}]/, "") end + def operation_name + @key.split(KEY_SEPARATOR)[-1] + end + def camelized_operation_name - key = @key.split(KEY_SEPARATOR)[-1] - inflector.camelize(key).gsub(/[^\p{Alnum}]/, "") + inflector.camelize(operation_name).gsub(/[^\p{Alnum}]/, "") end def camelized_parent_operation_name(slice = nil) @@ -78,26 +80,33 @@ def directory(slice = nil) end end - def generate_file(context, slice = nil) + def generate_file(slice = nil) if slice slice_directory = fs.join("slices", slice) raise MissingSliceError.new(slice) unless fs.directory?(slice_directory) end - class_definition = RubyFileGenerator.class( + fs.mkdir(directory(slice)) + + if @namespaces.none? + out.puts( + " Generating a top-level operation. " \ + "To generate into a directory, add a namespace: `my_namespace.#{operation_name}`" + ) + end + + path = fs.join(directory(slice), "#{operation_name}.rb") + + fs.write(path, class_definition(slice)) + end + + def class_definition(slice) + RubyFileGenerator.class( camelized_operation_name, parent_class: camelized_parent_operation_name(slice), modules: camelized_modules(slice), methods: {call: nil} ) - - fs.mkdir(directory(slice)) - - if context.namespaces.none? - out.puts(" Generating a top-level operation. To generate into a directory, add a namespace: `my_namespace.#{context.name}`") - end - - fs.write(fs.join(directory(slice), "#{context.name}.rb"), class_definition) end end end diff --git a/lib/hanami/cli/generators/app/operation_context.rb b/lib/hanami/cli/generators/app/operation_context.rb deleted file mode 100644 index 03ac07a2..00000000 --- a/lib/hanami/cli/generators/app/operation_context.rb +++ /dev/null @@ -1,83 +0,0 @@ -# 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 From 79699f0b700766334594bb592efcc63b14098292 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 14:01:06 -0600 Subject: [PATCH 28/56] Remove namespaces instance variable --- lib/hanami/cli/generators/app/operation.rb | 66 ++++++++++------------ 1 file changed, 29 insertions(+), 37 deletions(-) diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index feb04853..c188c763 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -22,22 +22,34 @@ def initialize(fs:, inflector:, out: $stdout) # @since 2.2.0 # @api private - def call(app, key, slice) - @app = app + def call(app_namespace, key, slice) + @camelized_app_name = inflector.camelize(app_namespace).gsub(/[^\p{Alnum}]/, "") @key = key - @namespaces = key.split(KEY_SEPARATOR)[..-2] + namespaces = key.split(KEY_SEPARATOR)[..-2] - generate_file(slice) + if slice + slice_directory = fs.join("slices", slice) + raise MissingSliceError.new(slice) unless fs.directory?(slice_directory) + end + + fs.mkdir(directory(slice, namespaces: namespaces)) + + if namespaces.none? + out.puts( + " Generating a top-level operation. " \ + "To generate into a directory, add a namespace: `my_namespace.#{operation_name}`" + ) + end + + path = fs.join(directory(slice, namespaces: namespaces), "#{operation_name}.rb") + + fs.write(path, class_definition(slice, namespaces: namespaces)) end private attr_reader :fs, :inflector, :out - def camelized_app_name - inflector.camelize(@app).gsub(/[^\p{Alnum}]/, "") - end - def operation_name @key.split(KEY_SEPARATOR)[-1] end @@ -54,57 +66,37 @@ def highest_level_module(slice) if slice inflector.camelize(slice).gsub(/[^\p{Alnum}]/, "") else - camelized_app_name + @camelized_app_name end end - def camelized_modules(slice = nil) - if @namespaces.any? - [highest_level_module(slice)].push(@namespaces.map { inflector.camelize(_1) }).flatten + def camelized_modules(slice = nil, namespaces:) + if namespaces.any? + [highest_level_module(slice)].push(namespaces.map { inflector.camelize(_1) }).flatten else [highest_level_module(slice)] end end - def directory(slice = nil) + def directory(slice = nil, namespaces:) base = if slice fs.join("slices", slice) else fs.join("app") end - if @namespaces.any? - fs.join(base, @namespaces) + if namespaces.any? + fs.join(base, namespaces) else fs.join(base) end end - def generate_file(slice = nil) - if slice - slice_directory = fs.join("slices", slice) - raise MissingSliceError.new(slice) unless fs.directory?(slice_directory) - end - - fs.mkdir(directory(slice)) - - if @namespaces.none? - out.puts( - " Generating a top-level operation. " \ - "To generate into a directory, add a namespace: `my_namespace.#{operation_name}`" - ) - end - - path = fs.join(directory(slice), "#{operation_name}.rb") - - fs.write(path, class_definition(slice)) - end - - def class_definition(slice) + def class_definition(slice, namespaces:) RubyFileGenerator.class( camelized_operation_name, parent_class: camelized_parent_operation_name(slice), - modules: camelized_modules(slice), + modules: camelized_modules(slice, namespaces: namespaces), methods: {call: nil} ) end From 3879144ddd02631e5bdbec1b0818a5f629eed789 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 14:11:27 -0600 Subject: [PATCH 29/56] Refactor to variables --- lib/hanami/cli/generators/app/operation.rb | 24 +++++++++------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index c188c763..a7f0849e 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -58,10 +58,6 @@ def camelized_operation_name inflector.camelize(operation_name).gsub(/[^\p{Alnum}]/, "") end - def camelized_parent_operation_name(slice = nil) - [highest_level_module(slice), "Operation"].join("::") - end - def highest_level_module(slice) if slice inflector.camelize(slice).gsub(/[^\p{Alnum}]/, "") @@ -70,14 +66,6 @@ def highest_level_module(slice) end end - def camelized_modules(slice = nil, namespaces:) - if namespaces.any? - [highest_level_module(slice)].push(namespaces.map { inflector.camelize(_1) }).flatten - else - [highest_level_module(slice)] - end - end - def directory(slice = nil, namespaces:) base = if slice fs.join("slices", slice) @@ -93,10 +81,18 @@ def directory(slice = nil, namespaces:) end def class_definition(slice, namespaces:) + camelized_modules = if namespaces.any? + [highest_level_module(slice)].push(namespaces.map { inflector.camelize(_1) }).flatten + else + [highest_level_module(slice)] + end + + parent_class = [highest_level_module(slice), "Operation"].join("::") + RubyFileGenerator.class( camelized_operation_name, - parent_class: camelized_parent_operation_name(slice), - modules: camelized_modules(slice, namespaces: namespaces), + parent_class: parent_class, + modules: camelized_modules, methods: {call: nil} ) end From 6f6fce30674587d9ccfad0fce1d5eca294cf26e7 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 14:21:26 -0600 Subject: [PATCH 30/56] Remove last temporary instance variable --- lib/hanami/cli/generators/app/operation.rb | 47 ++++++++++------------ 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index a7f0849e..93200ddf 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -23,8 +23,8 @@ def initialize(fs:, inflector:, out: $stdout) # @since 2.2.0 # @api private def call(app_namespace, key, slice) - @camelized_app_name = inflector.camelize(app_namespace).gsub(/[^\p{Alnum}]/, "") - @key = key + camelized_app_name = inflector.camelize(app_namespace).gsub(/[^\p{Alnum}]/, "") + operation_name = key.split(KEY_SEPARATOR)[-1] namespaces = key.split(KEY_SEPARATOR)[..-2] if slice @@ -32,7 +32,8 @@ def call(app_namespace, key, slice) raise MissingSliceError.new(slice) unless fs.directory?(slice_directory) end - fs.mkdir(directory(slice, namespaces: namespaces)) + directory = directory(slice, namespaces: namespaces) + fs.mkdir(directory) if namespaces.none? out.puts( @@ -41,30 +42,24 @@ def call(app_namespace, key, slice) ) end - path = fs.join(directory(slice, namespaces: namespaces), "#{operation_name}.rb") + highest_level_module = + if slice + inflector.camelize(slice).gsub(/[^\p{Alnum}]/, "") + else + camelized_app_name + end - fs.write(path, class_definition(slice, namespaces: namespaces)) - end - - private + path = fs.join(directory, "#{operation_name}.rb") - attr_reader :fs, :inflector, :out + file_contents = class_definition(highest_level_module: highest_level_module, + operation_name: operation_name, namespaces: namespaces) - def operation_name - @key.split(KEY_SEPARATOR)[-1] + fs.write(path, file_contents) end - def camelized_operation_name - inflector.camelize(operation_name).gsub(/[^\p{Alnum}]/, "") - end + private - def highest_level_module(slice) - if slice - inflector.camelize(slice).gsub(/[^\p{Alnum}]/, "") - else - @camelized_app_name - end - end + attr_reader :fs, :inflector, :out def directory(slice = nil, namespaces:) base = if slice @@ -80,14 +75,16 @@ def directory(slice = nil, namespaces:) end end - def class_definition(slice, namespaces:) + def class_definition(highest_level_module:, operation_name:, namespaces:) + camelized_operation_name = inflector.camelize(operation_name).gsub(/[^\p{Alnum}]/, "") + camelized_modules = if namespaces.any? - [highest_level_module(slice)].push(namespaces.map { inflector.camelize(_1) }).flatten + [highest_level_module].push(namespaces.map { inflector.camelize(_1) }).flatten else - [highest_level_module(slice)] + [highest_level_module] end - parent_class = [highest_level_module(slice), "Operation"].join("::") + parent_class = [highest_level_module, "Operation"].join("::") RubyFileGenerator.class( camelized_operation_name, From 30c2a946329a326b2778a8cd5797f18c99c142e4 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 14:33:18 -0600 Subject: [PATCH 31/56] Refactor --- lib/hanami/cli/generators/app/operation.rb | 68 +++++++++++----------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index 93200ddf..df4e9c0d 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -23,37 +23,22 @@ def initialize(fs:, inflector:, out: $stdout) # @since 2.2.0 # @api private def call(app_namespace, key, slice) - camelized_app_name = inflector.camelize(app_namespace).gsub(/[^\p{Alnum}]/, "") operation_name = key.split(KEY_SEPARATOR)[-1] namespaces = key.split(KEY_SEPARATOR)[..-2] + container_namespace = slice || app_namespace - if slice - slice_directory = fs.join("slices", slice) - raise MissingSliceError.new(slice) unless fs.directory?(slice_directory) - end + raise_missing_slice_error_if_missing(slice) if slice + print_namespace_recommendation(operation_name) if namespaces.none? directory = directory(slice, namespaces: namespaces) - fs.mkdir(directory) - - if namespaces.none? - out.puts( - " Generating a top-level operation. " \ - "To generate into a directory, add a namespace: `my_namespace.#{operation_name}`" - ) - end - - highest_level_module = - if slice - inflector.camelize(slice).gsub(/[^\p{Alnum}]/, "") - else - camelized_app_name - end - path = fs.join(directory, "#{operation_name}.rb") + fs.mkdir(directory) - file_contents = class_definition(highest_level_module: highest_level_module, - operation_name: operation_name, namespaces: namespaces) - + file_contents = class_definition( + container_namespace: container_namespace, + operation_name: operation_name, + namespaces: namespaces, + ) fs.write(path, file_contents) end @@ -75,24 +60,39 @@ def directory(slice = nil, namespaces:) end end - def class_definition(highest_level_module:, operation_name:, namespaces:) - camelized_operation_name = inflector.camelize(operation_name).gsub(/[^\p{Alnum}]/, "") + def class_definition(container_namespace:, operation_name:, namespaces:) + camelized_modules = namespaces + .map { camelize(_1) } + .compact + .prepend(camelize(container_namespace)) - camelized_modules = if namespaces.any? - [highest_level_module].push(namespaces.map { inflector.camelize(_1) }).flatten - else - [highest_level_module] - end - - parent_class = [highest_level_module, "Operation"].join("::") + parent_class = [camelize(container_namespace), "Operation"].join("::") RubyFileGenerator.class( - camelized_operation_name, + camelize(operation_name), parent_class: parent_class, modules: camelized_modules, methods: {call: nil} ) end + + def camelize(input) + inflector.camelize(input).gsub(/[^\p{Alnum}]/, "") + end + + def print_namespace_recommendation(operation_name) + out.puts( + " Generating a top-level operation. " \ + "To generate into a directory, add a namespace: `my_namespace.#{operation_name}`" + ) + end + + def raise_missing_slice_error_if_missing(slice) + if slice + slice_directory = fs.join("slices", slice) + raise MissingSliceError.new(slice) unless fs.directory?(slice_directory) + end + end end end end From da093d40095c08e8a7c9453300ae6e3c8713abd8 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 14:42:37 -0600 Subject: [PATCH 32/56] More refactoring, for clarity --- lib/hanami/cli/generators/app/operation.rb | 26 ++++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index df4e9c0d..4ece5bd3 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -24,20 +24,20 @@ def initialize(fs:, inflector:, out: $stdout) # @api private def call(app_namespace, key, slice) operation_name = key.split(KEY_SEPARATOR)[-1] - namespaces = key.split(KEY_SEPARATOR)[..-2] + local_namespaces = key.split(KEY_SEPARATOR)[..-2] container_namespace = slice || app_namespace raise_missing_slice_error_if_missing(slice) if slice - print_namespace_recommendation(operation_name) if namespaces.none? + print_namespace_recommendation(operation_name) if local_namespaces.none? - directory = directory(slice, namespaces: namespaces) + directory = directory(slice, local_namespaces: local_namespaces) path = fs.join(directory, "#{operation_name}.rb") fs.mkdir(directory) file_contents = class_definition( - container_namespace: container_namespace, operation_name: operation_name, - namespaces: namespaces, + container_namespace: container_namespace, + local_namespaces: local_namespaces, ) fs.write(path, file_contents) end @@ -46,27 +46,29 @@ def call(app_namespace, key, slice) attr_reader :fs, :inflector, :out - def directory(slice = nil, namespaces:) + def directory(slice = nil, local_namespaces:) base = if slice fs.join("slices", slice) else fs.join("app") end - if namespaces.any? - fs.join(base, namespaces) + if local_namespaces.any? + fs.join(base, local_namespaces) else fs.join(base) end end - def class_definition(container_namespace:, operation_name:, namespaces:) - camelized_modules = namespaces + def class_definition(operation_name:, container_namespace:, local_namespaces:) + container_module = camelize(container_namespace) + + camelized_modules = local_namespaces .map { camelize(_1) } .compact - .prepend(camelize(container_namespace)) + .prepend(container_module) - parent_class = [camelize(container_namespace), "Operation"].join("::") + parent_class = [container_module, "Operation"].join("::") RubyFileGenerator.class( camelize(operation_name), From 40237de433509a4ccf424aa24c9f0fce3f36b49a Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 14:44:32 -0600 Subject: [PATCH 33/56] Rename variable for clarity --- lib/hanami/cli/generators/app/operation.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index 4ece5bd3..0e00d615 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -63,7 +63,7 @@ def directory(slice = nil, local_namespaces:) def class_definition(operation_name:, container_namespace:, local_namespaces:) container_module = camelize(container_namespace) - camelized_modules = local_namespaces + modules = local_namespaces .map { camelize(_1) } .compact .prepend(container_module) @@ -73,7 +73,7 @@ def class_definition(operation_name:, container_namespace:, local_namespaces:) RubyFileGenerator.class( camelize(operation_name), parent_class: parent_class, - modules: camelized_modules, + modules: modules, methods: {call: nil} ) end From f0cf827a422808e0447fdd74eaf2a85d829c555c Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 14:45:24 -0600 Subject: [PATCH 34/56] Rename helper method --- lib/hanami/cli/generators/app/operation.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index 0e00d615..bab2c45f 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -61,24 +61,24 @@ def directory(slice = nil, local_namespaces:) end def class_definition(operation_name:, container_namespace:, local_namespaces:) - container_module = camelize(container_namespace) + container_module = normalize(container_namespace) modules = local_namespaces - .map { camelize(_1) } + .map { normalize(_1) } .compact .prepend(container_module) parent_class = [container_module, "Operation"].join("::") RubyFileGenerator.class( - camelize(operation_name), + normalize(operation_name), parent_class: parent_class, modules: modules, methods: {call: nil} ) end - def camelize(input) + def normalize(input) inflector.camelize(input).gsub(/[^\p{Alnum}]/, "") end From 2dff13087fa9dd4f2b6b6f90b068f02d7d5dfeb3 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 17:06:40 -0600 Subject: [PATCH 35/56] Simplify RubyFileGenerator, support older versions --- lib/hanami/cli/ruby_file_generator.rb | 148 +--- .../hanami/cli/ruby_file_generator_spec.rb | 748 +++++------------- 2 files changed, 193 insertions(+), 703 deletions(-) diff --git a/lib/hanami/cli/ruby_file_generator.rb b/lib/hanami/cli/ruby_file_generator.rb index d0fe77f6..ad52ec18 100644 --- a/lib/hanami/cli/ruby_file_generator.rb +++ b/lib/hanami/cli/ruby_file_generator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "prism" +require "ripper" module Hanami module CLI @@ -39,26 +39,14 @@ def initialize( class_name: nil, parent_class: nil, modules: [], - requires: [], - relative_requires: [], - methods: {}, - includes: [], - top_contents: [], - magic_comments: {}, - ivars: [] + header: [], + body: [] ) @class_name = class_name @parent_class = parent_class @modules = modules - @requires = requires - @relative_requires = relative_requires - @methods = methods - @includes = includes - @top_contents = top_contents - @magic_comments = magic_comments.merge(frozen_string_literal).compact.sort - @ivar_names = parse_ivar_names!(ivars) - - raise DuplicateInitializeMethodError if methods.key?(:initialize) && ivars.any? + @header = header.any? && (header + [""]) || header + @body = body end # rubocop:enable Metrics/ParameterLists @@ -79,7 +67,7 @@ def self.module(*names, **) def to_s definition = lines(modules).map { |line| "#{line}\n" }.join - source_code = [file_directives, definition].flatten.join("\n") + source_code = [header, definition].flatten.join("\n") ensure_parseable!(source_code) end @@ -91,13 +79,8 @@ def to_s :class_name, :parent_class, :modules, - :requires, - :relative_requires, - :methods, - :includes, - :top_contents, - :magic_comments, - :ivar_names + :header, + :body ) def lines(remaining_modules) @@ -107,7 +90,7 @@ def lines(remaining_modules) elsif class_name class_lines else - [] + body end end @@ -119,42 +102,12 @@ def with_module_lines(module_name, contents_lines) ] end - def file_directives - [magic_comments_lines, import_lines].compact - end - - def magic_comments_lines - lines = magic_comments - .map { |magic_key, magic_value| "# #{magic_key}: #{magic_value}" } - add_empty_line_if_any(lines) - end - - def frozen_string_literal - {frozen_string_literal: true} - end - - def import_lines - lines = [requires_lines, relative_requires_lines].flatten.compact - add_empty_line_if_any(lines) - end - - def requires_lines - requires.map do |require| - %(require "#{require}") - end - end - - def relative_requires_lines - relative_requires.map do |require| - %(require_relative "#{require}") - end - end def class_lines if class_name [ class_definition, - *class_contents_lines, + *body.map { |line| indent(line) }, "end" ].compact else @@ -162,56 +115,6 @@ def class_lines end end - def includes_lines - if includes.any? - includes.map do |include| - "include #{include}" - end - end - end - - def top_contents_lines - if top_contents.any? - top_contents - end - end - - def class_contents_lines - line_groups = [ - includes_lines, - top_contents_lines, - initialize_lines, - *methods_lines, - *private_contents_lines - ].compact - add_empty_line_between_groups(line_groups).flatten.map { |line| indent(line) } - end - - def initialize_lines - if ivar_names.any? - [ - method_definition("initialize", ivar_names.map { |ivar| "#{ivar}:" }), - ivar_names.map { |ivar_name| indent("@#{ivar_name} = #{ivar_name}") }.flatten, - "end" - ] - end - end - - def private_contents_lines - if ivar_names.any? - [ - "private", - "attr_reader #{ivar_names.map { |ivar| ":#{ivar}" }.join(', ')}" - ] - end - end - - def methods_lines - methods.map do |method_name, args| - [method_definition(method_name, args), "end"] - end - end - def class_definition if parent_class "class #{class_name} < #{parent_class}" @@ -220,14 +123,6 @@ def class_definition end end - def method_definition(method_name, args) - if args - "def #{method_name}(#{args.join(', ')})" - else - "def #{method_name}" - end - end - def indent(line) if line.strip.empty? "" @@ -236,34 +131,15 @@ def indent(line) end end - def parse_ivar_names!(ivars) - if ivars.all? { |ivar| ivar.start_with?("@") } - ivars.map { |ivar| ivar.to_s.delete_prefix("@") } - else - raise InvalidInstanceVariablesError - end - end - def ensure_parseable!(source_code) - parse_result = Prism.parse(source_code) + parse_result = Ripper.sexp(source_code) - if parse_result.success? + if parse_result source_code else raise GeneratedUnparseableCodeError.new(source_code) end end - - def add_empty_line_if_any(lines) - if lines.any? - lines << "" - end - end - - def add_empty_line_between_groups(line_groups) - # We add an empty line after every group then remove the last one - line_groups.flat_map { |line_group| [line_group, ""] }[0...-1] - end end end end diff --git a/spec/unit/hanami/cli/ruby_file_generator_spec.rb b/spec/unit/hanami/cli/ruby_file_generator_spec.rb index a9c32d32..03cb662f 100644 --- a/spec/unit/hanami/cli/ruby_file_generator_spec.rb +++ b/spec/unit/hanami/cli/ruby_file_generator_spec.rb @@ -2,580 +2,276 @@ RSpec.describe Hanami::CLI::RubyFileGenerator do describe ".class" do - describe "no methods" do - describe "top-level" do - it "generates class without parent class" do - expect( - described_class.class("Greeter") - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - class Greeter - end - OUTPUT - ) - ) - end - - it "generates class with parent class" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - parent_class: "BaseService", - ).to_s - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - class Greeter < BaseService - end - OUTPUT - ) + describe "without modules" do + it "generates class without parent class" do + expect( + described_class.class("Greeter") + ).to( + eq( + <<~OUTPUT + class Greeter + end + OUTPUT ) - end + ) end - describe "with single module" do - it "generates class without parent class" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - modules: %w[Services], - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - module Services - class Greeter - end - end - OUTPUT - ) + it "generates class with parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + ).to_s + ).to( + eq( + <<~OUTPUT + class Greeter < BaseService + end + OUTPUT ) - end - - it "generates class with parent class" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - parent_class: "BaseService", - modules: %w[Services] - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true + ) + end - module Services - class Greeter < BaseService - end - end - OUTPUT - ) + it "generates class with parent class and body" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + body: %w[foo bar] + ).to_s + ).to( + eq( + <<~OUTPUT + class Greeter < BaseService + foo + bar + end + OUTPUT ) - end + ) end + end - describe "with two modules" do - it "generates class without parent class" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - modules: %w[Admin Services], - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - module Admin - module Services - class Greeter - end - end - end - OUTPUT - ) + describe "with 1 module" do + it "generates class without parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: %w[Services], ) - end - - it "generates class with parent class" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - parent_class: "BaseService", - modules: %w[Admin Services] - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - module Admin - module Services - class Greeter < BaseService - end - end + ).to( + eq( + <<~OUTPUT + module Services + class Greeter end - OUTPUT - ) + end + OUTPUT ) - end + ) end - describe "with three modules" do - it "generates class without parent class" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - modules: %w[Internal Admin Services] - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - module Internal - module Admin - module Services - class Greeter - end - end - end - end - OUTPUT - ) + it "generates class with parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + modules: %w[Services] ) - end - - it "generates class with parent class" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - parent_class: "BaseService", - modules: %w[Internal Admin Services] - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - module Internal - module Admin - module Services - class Greeter < BaseService - end - end - end + ).to( + eq( + <<~OUTPUT + module Services + class Greeter < BaseService end - OUTPUT - ) + end + OUTPUT ) - end + ) end - end - describe "with methods" do - describe "top-level" do - it "generates class without parent class and call method with no args" do - expect( - described_class.class("Greeter", methods: {call: nil}) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - class Greeter - def call - end - end - OUTPUT - ) + it "generates class with parent class, body and header" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + modules: %w[Services], + header: ["# hello world"], + body: %w[foo bar] ) - end - - it "generates class with parent class and call method with 1 arg" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - parent_class: "BaseService", - methods: {call: ["args"]} - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true + ).to( + eq( + <<~OUTPUT + # hello world + module Services class Greeter < BaseService - def call(args) - end + foo + bar end - OUTPUT - ) + end + OUTPUT ) - end + ) end + end - describe "with single module" do - it "generates class without parent class and call methods with 2 args" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - modules: %w[Services], - methods: {call: %w[request response]} - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - + describe "with two modules" do + it "generates class without parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: %w[Admin Services], + ) + ).to( + eq( + <<~OUTPUT + module Admin module Services class Greeter - def call(request, response) - end end end - OUTPUT - ) + end + OUTPUT ) - end - - it "generates class with parent class and call method with required keyword args" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - parent_class: "BaseService", - modules: %w[Services], - methods: {call: %w[request: response:]} - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true + ) + end + it "generates class with parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + modules: %w[Admin Services] + ) + ).to( + eq( + <<~OUTPUT + module Admin module Services class Greeter < BaseService - def call(request:, response:) - end end end - OUTPUT - ) + end + OUTPUT ) - end + ) end + end - describe "with two modules" do - it "generates class without parent class and call method with mix of args" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - modules: %w[Admin Services], - methods: {call: ["env", "request:", "response:", "context: nil"]} - ).to_s - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - + describe "with three modules" do + it "generates class without parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + modules: %w[Internal Admin Services] + ) + ).to( + eq( + <<~OUTPUT + module Internal module Admin module Services class Greeter - def call(env, request:, response:, context: nil) - end end end end - OUTPUT - ) + end + OUTPUT ) - end - - it "generates class with parent class and two methods" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - parent_class: "BaseService", - modules: %w[Admin Services], - methods: {initialize: ["context"], call: ["args"]} - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true + ) + end + it "generates class with parent class" do + expect( + Hanami::CLI::RubyFileGenerator.class( + "Greeter", + parent_class: "BaseService", + modules: %w[Internal Admin Services] + ) + ).to( + eq( + <<~OUTPUT + module Internal module Admin module Services class Greeter < BaseService - def initialize(context) - end - - def call(args) - end end end end - OUTPUT - ) - ) - end - end - - describe "with three modules" do - it "generates class without parent class, with ivars and method" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - modules: %w[Internal Admin Services], - ivars: [:@name, :@birthdate], - methods: {call: [:env]} - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - module Internal - module Admin - module Services - class Greeter - def initialize(name:, birthdate:) - @name = name - @birthdate = birthdate - end - - def call(env) - end - - private - - attr_reader :name, :birthdate - end - end - end - end - OUTPUT - ) - ) - end - - it "raises error when ivars don't lead with @" do - expect { - described_class.class("Greeter", ivars: [:name]) - }.to(raise_error(Hanami::CLI::RubyFileGenerator::InvalidInstanceVariablesError)) - end - - it "raises error when 'initialize' method is specified and ivars are present" do - expect { - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - ivars: [:@name], - methods: {initialize: nil} - ) - }.to(raise_error(Hanami::CLI::RubyFileGenerator::DuplicateInitializeMethodError)) - end - - it "generates class with parent class, and requires" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - parent_class: "BaseService", - modules: %w[Internal Admin Services], - requires: ["roobi/fake"] - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - require "roobi/fake" - - module Internal - module Admin - module Services - class Greeter < BaseService - end - end - end - end - OUTPUT - ) + end + OUTPUT ) - end + ) end + end + end - describe "with includes" do - it "generates class with includes" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - includes: ["Enumerable", %(Import["external.api"])] - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - class Greeter - include Enumerable - include Import["external.api"] - end - OUTPUT - ) - ) - end - - it "generates class with includes and ivars" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - includes: ["Enumerable", %(Import["external.api"])], - ivars: [:@name] - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - class Greeter - include Enumerable - include Import["external.api"] - - def initialize(name:) - @name = name - end - - private - - attr_reader :name - end - OUTPUT - ) - ) - end - - it "generates class with includes and one method" do + describe ".module" do + describe "without frozen_string_literal" do + describe "top-level" do + it "generates module by itself" do expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - includes: ["Enumerable", %(Import["external.api"])], - methods: {call: ["name"]} - ) + described_class.module("Greetable") ).to( eq( <<~OUTPUT - # frozen_string_literal: true - - class Greeter - include Enumerable - include Import["external.api"] - - def call(name) - end + module Greetable end OUTPUT ) ) end - end - describe "with inline syntax name for parent, module, class" do - it "generates class with inline-syntax" do + it "generates modules nested in a module, from array" do expect( - Hanami::CLI::RubyFileGenerator.class( - "Services::Greeter", - parent_class: "Internal::BaseService", - modules: ["Internal::Admin"] - ) + described_class.module(%w[External Greetable]) ).to( eq( <<~OUTPUT - # frozen_string_literal: true - - module Internal::Admin - class Services::Greeter < Internal::BaseService + module External + module Greetable end end OUTPUT ) ) end - end - describe "with magic comment" do - it "generates class with custom magic comment" do + it "generates modules nested in a module, from array with header and body" do expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - modules: ["Internal"], - magic_comments: {value: true} + described_class.module( + %w[External Greetable], + header: ["# hello world"], + body: %w[foo bar] ) ).to( eq( <<~OUTPUT - # frozen_string_literal: true - # value: true + # hello world - module Internal - class Greeter + module External + module Greetable + foo + bar end end OUTPUT ) ) end - end - - describe "with top contents" do - it "generates simple class with only top contents as comment" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Foo", - top_contents: ["# code goes here"] - ) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - class Foo - # code goes here - end - OUTPUT - ) - ) - end - it "generates class with top contents in correct spot" do + it "generates modules nested in a module, from list" do expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - includes: ["Validatable"], - ivars: [:@name], - top_contents: ["before_call :validate"] - ) + described_class.module("Admin", "External", "Greetable") ).to( eq( <<~OUTPUT - # frozen_string_literal: true - - class Greeter - include Validatable - - before_call :validate - - def initialize(name:) - @name = name + module Admin + module External + module Greetable + end end - - private - - attr_reader :name end OUTPUT ) @@ -583,93 +279,11 @@ def initialize(name:) end end end + end - it "generates class with sorted custom magic comments, including frozen_string_literal" do - expect( - Hanami::CLI::RubyFileGenerator.class( - "Greeter", - modules: ["Internal"], - magic_comments: {abc: 123, value: true} - ) - ).to( - eq( - <<~OUTPUT - # abc: 123 - # frozen_string_literal: true - # value: true - - module Internal - class Greeter - end - end - OUTPUT - ) - ) - end - - it "fails to generate unparseable ruby code" do - expect { described_class.class("%%Greeter") }.to( - raise_error(Hanami::CLI::RubyFileGenerator::GeneratedUnparseableCodeError) - ) - end - - describe ".module" do - describe "no methods" do - describe "without frozen_string_literal" do - describe "top-level" do - it "generates module by itself" do - expect( - described_class.module("Greetable") - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - module Greetable - end - OUTPUT - ) - ) - end - - it "generates modules nested in a module, from arrray" do - expect( - described_class.module(%w[External Greetable]) - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - module External - module Greetable - end - end - OUTPUT - ) - ) - end - - it "generates modules nested in a module, from list" do - expect( - described_class.module("Admin", "External", "Greetable") - ).to( - eq( - <<~OUTPUT - # frozen_string_literal: true - - module Admin - module External - module Greetable - end - end - end - OUTPUT - ) - ) - end - end - end - end - end + it "fails to generate unparseable ruby code" do + expect { described_class.class("%%Greeter") }.to( + raise_error(Hanami::CLI::RubyFileGenerator::GeneratedUnparseableCodeError) + ) end end From 36c7b3bfc94c22ee4f812dfbc4ea65b3f44627b9 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 17:18:27 -0600 Subject: [PATCH 36/56] Convert Operation generator to use simplified RubyFileGenerator --- lib/hanami/cli/generators/app/operation.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index bab2c45f..547ae70d 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -74,7 +74,8 @@ def class_definition(operation_name:, container_namespace:, local_namespaces:) normalize(operation_name), parent_class: parent_class, modules: modules, - methods: {call: nil} + body: ["def call", "end"], + header: ["# frozen_string_literal: true"], ) end From 2a4f0b6ed7abaf6e27507afeabcdd8429aeaeb52 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 17:21:21 -0600 Subject: [PATCH 37/56] Remove un-used errors --- lib/hanami/cli/ruby_file_generator.rb | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/lib/hanami/cli/ruby_file_generator.rb b/lib/hanami/cli/ruby_file_generator.rb index ad52ec18..0e9188df 100644 --- a/lib/hanami/cli/ruby_file_generator.rb +++ b/lib/hanami/cli/ruby_file_generator.rb @@ -5,18 +5,6 @@ module Hanami module CLI class RubyFileGenerator - - class InvalidInstanceVariablesError < Error - def initialize - end - end - - class DuplicateInitializeMethodError < Error - def initialize - super("Initialize method cannot be defined if instance variables are present") - end - end - class GeneratedUnparseableCodeError < Error def initialize(source_code) super( @@ -32,6 +20,7 @@ def initialize(source_code) ) end end + INDENT = " " # rubocop:disable Metrics/ParameterLists From 0223beaa459d2f7892acc45fc944bff1cfb7e907 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 17:21:56 -0600 Subject: [PATCH 38/56] Refactor --- lib/hanami/cli/ruby_file_generator.rb | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/hanami/cli/ruby_file_generator.rb b/lib/hanami/cli/ruby_file_generator.rb index 0e9188df..cf03a116 100644 --- a/lib/hanami/cli/ruby_file_generator.rb +++ b/lib/hanami/cli/ruby_file_generator.rb @@ -91,17 +91,12 @@ def with_module_lines(module_name, contents_lines) ] end - def class_lines - if class_name - [ - class_definition, - *body.map { |line| indent(line) }, - "end" - ].compact - else - [] - end + [ + class_definition, + *body.map { |line| indent(line) }, + "end" + ].compact end def class_definition From 8e458ce00c917bdde7c687f71ed15ff7f216c568 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 17:25:43 -0600 Subject: [PATCH 39/56] Older kwargs forwarding style --- lib/hanami/cli/ruby_file_generator.rb | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/hanami/cli/ruby_file_generator.rb b/lib/hanami/cli/ruby_file_generator.rb index cf03a116..8b6acf4e 100644 --- a/lib/hanami/cli/ruby_file_generator.rb +++ b/lib/hanami/cli/ruby_file_generator.rb @@ -23,7 +23,6 @@ def initialize(source_code) INDENT = " " - # rubocop:disable Metrics/ParameterLists def initialize( class_name: nil, parent_class: nil, @@ -34,23 +33,22 @@ def initialize( @class_name = class_name @parent_class = parent_class @modules = modules - @header = header.any? && (header + [""]) || header + @header = (header.any? && (header + [""])) || header @body = body end - # rubocop:enable Metrics/ParameterLists - def self.class(class_name, **) - new(class_name: class_name, **).to_s + def self.class(class_name, **args) + new(class_name: class_name, **args).to_s end - def self.module(*names, **) + def self.module(*names, **args) module_names = if names.first.is_a?(Array) - names.first - else - names - end + names.first + else + names + end - new(modules: module_names, class_name: nil, parent_class: nil, **).to_s + new(modules: module_names, class_name: nil, parent_class: nil, **args).to_s end def to_s @@ -61,7 +59,6 @@ def to_s ensure_parseable!(source_code) end - private attr_reader( From d4e13bd7ee09cbc39de913c088b23649c165cce8 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 17:40:56 -0600 Subject: [PATCH 40/56] Refactor --- lib/hanami/cli/ruby_file_generator.rb | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/hanami/cli/ruby_file_generator.rb b/lib/hanami/cli/ruby_file_generator.rb index 8b6acf4e..8ea85b91 100644 --- a/lib/hanami/cli/ruby_file_generator.rb +++ b/lib/hanami/cli/ruby_file_generator.rb @@ -53,10 +53,9 @@ def self.module(*names, **args) def to_s definition = lines(modules).map { |line| "#{line}\n" }.join - source_code = [header, definition].flatten.join("\n") - ensure_parseable!(source_code) + source_code end private @@ -113,11 +112,7 @@ def indent(line) end def ensure_parseable!(source_code) - parse_result = Ripper.sexp(source_code) - - if parse_result - source_code - else + unless Ripper.sexp(source_code) raise GeneratedUnparseableCodeError.new(source_code) end end From f9221f04b0d4a58cdd693cea511a44d8d78bfb9e Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 24 Jun 2024 17:44:10 -0600 Subject: [PATCH 41/56] Rename variable --- lib/hanami/cli/generators/app/operation.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index b5a3851c..5d961ce7 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -82,8 +82,8 @@ def class_definition(operation_name:, container_namespace:, local_namespaces:) ) end - def normalize(input) - inflector.camelize(input).gsub(/[^\p{Alnum}]/, "") + def normalize(name) + inflector.camelize(name).gsub(/[^\p{Alnum}]/, "") end def print_namespace_recommendation(operation_name) From 5f5887b39b32c36d9d32a4fed79ca724000b3900 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Mon, 1 Jul 2024 13:17:48 -0600 Subject: [PATCH 42/56] Add explanatory comment Add dry-monads include for slice base action --- .../cli/generators/app/slice/action.erb | 4 +++ lib/hanami/cli/generators/context.rb | 6 +++++ lib/hanami/cli/generators/gem/app/action.erb | 1 + .../cli/commands/app/generate/slice_spec.rb | 27 +++++++++++++++++++ spec/unit/hanami/cli/commands/gem/new_spec.rb | 1 + 5 files changed, 39 insertions(+) diff --git a/lib/hanami/cli/generators/app/slice/action.erb b/lib/hanami/cli/generators/app/slice/action.erb index 3d0a12f8..94a55360 100644 --- a/lib/hanami/cli/generators/app/slice/action.erb +++ b/lib/hanami/cli/generators/app/slice/action.erb @@ -3,5 +3,9 @@ module <%= camelized_slice_name %> class Action < <%= camelized_app_name %>::Action + <%- if bundled_dry_monads? -%> + # Provide `Success` and `Failure` for pattern matching on operation results + include Dry::Monads[:result] + <%- end -%> end end diff --git a/lib/hanami/cli/generators/context.rb b/lib/hanami/cli/generators/context.rb index eb57b63b..fed0e925 100644 --- a/lib/hanami/cli/generators/context.rb +++ b/lib/hanami/cli/generators/context.rb @@ -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 # diff --git a/lib/hanami/cli/generators/gem/app/action.erb b/lib/hanami/cli/generators/gem/app/action.erb index 56eab2e4..e7fa33c5 100644 --- a/lib/hanami/cli/generators/gem/app/action.erb +++ b/lib/hanami/cli/generators/gem/app/action.erb @@ -5,6 +5,7 @@ require "hanami/action" module <%= camelized_app_name %> class Action < Hanami::Action + # Provide `Success` and `Failure` for pattern matching on operation results include Dry::Monads[:result] end end diff --git a/spec/unit/hanami/cli/commands/app/generate/slice_spec.rb b/spec/unit/hanami/cli/commands/app/generate/slice_spec.rb index c05825f2..54b1a1f4 100644 --- a/spec/unit/hanami/cli/commands/app/generate/slice_spec.rb +++ b/spec/unit/hanami/cli/commands/app/generate/slice_spec.rb @@ -242,6 +242,33 @@ class Routes < Hanami::Routes end end + context "with dry-monads bundled" do + before do + allow(Hanami).to receive(:bundled?).with("dry-monads").and_return(bundled_assets) + end + + it "generates a slice with an operation that includes dry-monads result" do + within_application_directory do + subject.call(name: slice) + + action = <<~CODE + # auto_register: false + # frozen_string_literal: true + + module Admin + class Action < #{app}::Action + # Provide `Success` and `Failure` for pattern matching on operation results + include Dry::Monads[:result] + end + end + CODE + + expect(fs.read("slices/#{slice}/action.rb")).to eq(action) + expect(output).to include("Created slices/#{slice}/action.rb") + end + end + end + private def within_application_directory diff --git a/spec/unit/hanami/cli/commands/gem/new_spec.rb b/spec/unit/hanami/cli/commands/gem/new_spec.rb index c535aaa2..ecf0711c 100644 --- a/spec/unit/hanami/cli/commands/gem/new_spec.rb +++ b/spec/unit/hanami/cli/commands/gem/new_spec.rb @@ -341,6 +341,7 @@ class Routes < Hanami::Routes module #{inflector.camelize(app)} class Action < Hanami::Action + # Provide `Success` and `Failure` for pattern matching on operation results include Dry::Monads[:result] end end From a051c7d4f6303f46ce88d7d62c4abfee9946d94f Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Fri, 5 Jul 2024 13:24:07 -0600 Subject: [PATCH 43/56] Fix base slice action --- .../cli/generators/app/slice/action.erb | 4 --- .../cli/commands/app/generate/slice_spec.rb | 27 ------------------- 2 files changed, 31 deletions(-) diff --git a/lib/hanami/cli/generators/app/slice/action.erb b/lib/hanami/cli/generators/app/slice/action.erb index 94a55360..3d0a12f8 100644 --- a/lib/hanami/cli/generators/app/slice/action.erb +++ b/lib/hanami/cli/generators/app/slice/action.erb @@ -3,9 +3,5 @@ module <%= camelized_slice_name %> class Action < <%= camelized_app_name %>::Action - <%- if bundled_dry_monads? -%> - # Provide `Success` and `Failure` for pattern matching on operation results - include Dry::Monads[:result] - <%- end -%> end end diff --git a/spec/unit/hanami/cli/commands/app/generate/slice_spec.rb b/spec/unit/hanami/cli/commands/app/generate/slice_spec.rb index 34d35c61..c05825f2 100644 --- a/spec/unit/hanami/cli/commands/app/generate/slice_spec.rb +++ b/spec/unit/hanami/cli/commands/app/generate/slice_spec.rb @@ -242,33 +242,6 @@ class Routes < Hanami::Routes end end - context "with dry-monads bundled" do - before do - allow(Hanami).to receive(:bundled?).with("dry-monads").and_return(bundled_assets) - end - - it "generates a slice with an action that includes dry-monads result" do - within_application_directory do - subject.call(name: slice) - - action = <<~CODE - # auto_register: false - # frozen_string_literal: true - - module Admin - class Action < #{app}::Action - # Provide `Success` and `Failure` for pattern matching on operation results - include Dry::Monads[:result] - end - end - CODE - - expect(fs.read("slices/#{slice}/action.rb")).to eq(action) - expect(output).to include("Created slices/#{slice}/action.rb") - end - end - end - private def within_application_directory From d0d02c7a1521472d0e288093272c1728c60e4f11 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Fri, 5 Jul 2024 13:24:49 -0600 Subject: [PATCH 44/56] Remove un-used ERB templates --- .../generators/app/operation/nested_app_operation.erb | 10 ---------- .../app/operation/nested_slice_operation.erb | 10 ---------- .../app/operation/top_level_app_operation.erb | 8 -------- .../app/operation/top_level_slice_operation.erb | 8 -------- 4 files changed, 36 deletions(-) delete mode 100644 lib/hanami/cli/generators/app/operation/nested_app_operation.erb delete mode 100644 lib/hanami/cli/generators/app/operation/nested_slice_operation.erb delete mode 100644 lib/hanami/cli/generators/app/operation/top_level_app_operation.erb delete mode 100644 lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb diff --git a/lib/hanami/cli/generators/app/operation/nested_app_operation.erb b/lib/hanami/cli/generators/app/operation/nested_app_operation.erb deleted file mode 100644 index f0b7a131..00000000 --- a/lib/hanami/cli/generators/app/operation/nested_app_operation.erb +++ /dev/null @@ -1,10 +0,0 @@ -# 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 diff --git a/lib/hanami/cli/generators/app/operation/nested_slice_operation.erb b/lib/hanami/cli/generators/app/operation/nested_slice_operation.erb deleted file mode 100644 index a9a448ad..00000000 --- a/lib/hanami/cli/generators/app/operation/nested_slice_operation.erb +++ /dev/null @@ -1,10 +0,0 @@ -# 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 diff --git a/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb b/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb deleted file mode 100644 index 0ce59f47..00000000 --- a/lib/hanami/cli/generators/app/operation/top_level_app_operation.erb +++ /dev/null @@ -1,8 +0,0 @@ -# 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 diff --git a/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb b/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb deleted file mode 100644 index 240448ab..00000000 --- a/lib/hanami/cli/generators/app/operation/top_level_slice_operation.erb +++ /dev/null @@ -1,8 +0,0 @@ -# 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 From 145f7ee4f8997c233d64421538c159d7b58118ea Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Fri, 5 Jul 2024 13:28:18 -0600 Subject: [PATCH 45/56] Remove OperationContext --- .../cli/generators/app/operation_context.rb | 83 ------------------- 1 file changed, 83 deletions(-) delete mode 100644 lib/hanami/cli/generators/app/operation_context.rb diff --git a/lib/hanami/cli/generators/app/operation_context.rb b/lib/hanami/cli/generators/app/operation_context.rb deleted file mode 100644 index 03ac07a2..00000000 --- a/lib/hanami/cli/generators/app/operation_context.rb +++ /dev/null @@ -1,83 +0,0 @@ -# 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 From b76942c424c83914bad3ea14a829283a76f7163a Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Fri, 5 Jul 2024 13:47:16 -0600 Subject: [PATCH 46/56] Ternary over and/or --- lib/hanami/cli/ruby_file_generator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/hanami/cli/ruby_file_generator.rb b/lib/hanami/cli/ruby_file_generator.rb index 8ea85b91..2e1582ff 100644 --- a/lib/hanami/cli/ruby_file_generator.rb +++ b/lib/hanami/cli/ruby_file_generator.rb @@ -33,7 +33,7 @@ def initialize( @class_name = class_name @parent_class = parent_class @modules = modules - @header = (header.any? && (header + [""])) || header + @header = header.any? ? (header + [""]) : [] @body = body end From 26a665b050d536dbf801cfb00f537bcd6adba2cc Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Fri, 5 Jul 2024 13:56:08 -0600 Subject: [PATCH 47/56] Fix missing 'end' from bad merge --- lib/hanami/cli/generators/app/operation.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index ed76a1c3..5d961ce7 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -43,6 +43,7 @@ def call(app_namespace, key, slice) local_namespaces: local_namespaces, ) fs.write(path, file_contents) + end private From b07cf3518e4bded7c9cf62c069a2b7ed6a1a259f Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Fri, 5 Jul 2024 13:56:57 -0600 Subject: [PATCH 48/56] Fix namespace recommendation --- lib/hanami/cli/generators/app/operation.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index 5d961ce7..0c2be8e1 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -88,7 +88,7 @@ def normalize(name) def print_namespace_recommendation(operation_name) out.puts( - " Generating a top-level operation. " \ + " Note: We generated a top-level operation. " \ "To generate into a directory, add a namespace: `my_namespace.#{operation_name}`" ) end From f649975e784eacf26666d89788fe44360299363e Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Thu, 11 Jul 2024 14:20:01 -0600 Subject: [PATCH 49/56] Extract App::Generate::Command --- lib/hanami/cli/command.rb | 2 +- .../cli/commands/app/generate/command.rb | 48 +++++++++++++++++++ .../cli/commands/app/generate/operation.rb | 24 ++-------- .../commands/app/generate/operation_spec.rb | 4 +- 4 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 lib/hanami/cli/commands/app/generate/command.rb diff --git a/lib/hanami/cli/command.rb b/lib/hanami/cli/command.rb index 87014b2d..67015466 100644 --- a/lib/hanami/cli/command.rb +++ b/lib/hanami/cli/command.rb @@ -27,7 +27,7 @@ def self.new( inflector: Dry::Inflector.new, **opts ) - super(out: out, err: err, fs: fs, inflector: inflector, **opts) + super end # Returns a new command. diff --git a/lib/hanami/cli/commands/app/generate/command.rb b/lib/hanami/cli/commands/app/generate/command.rb new file mode 100644 index 00000000..b16c45a2 --- /dev/null +++ b/lib/hanami/cli/commands/app/generate/command.rb @@ -0,0 +1,48 @@ +# 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 Command < App::Command + argument :name, required: true, desc: "Name" + option :slice, required: false, desc: "Slice name" + + attr_reader :generator + private :generator + + # @since 2.2.0 + # @api private + def initialize( + fs:, + inflector:, + generator_class: nil, + **opts + ) + raise "Provide a generator class (that takes fs and inflector)" if generator_class.nil? + + super(fs: fs, inflector: inflector, **opts) + @generator = generator_class.new(fs: fs, inflector: inflector, out: out) + end + + # @since 2.2.0 + # @api private + def call(name:, slice: nil, **) + normalized_slice = inflector.underscore(Shellwords.shellescape(slice)) if slice + generator.call(app.namespace, name, normalized_slice) + end + end + end + end + end + end +end diff --git a/lib/hanami/cli/commands/app/generate/operation.rb b/lib/hanami/cli/commands/app/generate/operation.rb index ad4fb374..dc838327 100644 --- a/lib/hanami/cli/commands/app/generate/operation.rb +++ b/lib/hanami/cli/commands/app/generate/operation.rb @@ -13,34 +13,16 @@ 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" - + class Operation < Generate::Command 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) + def initialize(**opts) + super(generator_class: Generators::App::Operation, **opts) end end end diff --git a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb index b190f68a..76970a59 100644 --- a/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb +++ b/spec/unit/hanami/cli/commands/app/generate/operation_spec.rb @@ -3,12 +3,12 @@ require "hanami" RSpec.describe Hanami::CLI::Commands::App::Generate::Operation, :app do - subject { described_class.new(fs: fs, inflector: inflector, generator: generator) } + subject { described_class.new(fs: fs, inflector: inflector, generator_class: generator_class, out: out) } let(:out) { StringIO.new } let(:fs) { Hanami::CLI::Files.new(memory: true, out: out) } let(:inflector) { Dry::Inflector.new } - let(:generator) { Hanami::CLI::Generators::App::Operation.new(fs: fs, inflector: inflector, out: out) } + let(:generator_class) { Hanami::CLI::Generators::App::Operation } let(:app) { Hanami.app.namespace } let(:dir) { inflector.underscore(app) } From 5239824cf241d7afe712ea668030809cf930bb37 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Thu, 11 Jul 2024 14:22:58 -0600 Subject: [PATCH 50/56] Specify full name, to use App::Command --- lib/hanami/cli/commands/app/generate/slice.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/hanami/cli/commands/app/generate/slice.rb b/lib/hanami/cli/commands/app/generate/slice.rb index eba1ab15..de673567 100644 --- a/lib/hanami/cli/commands/app/generate/slice.rb +++ b/lib/hanami/cli/commands/app/generate/slice.rb @@ -12,7 +12,7 @@ module App module Generate # @since 2.0.0 # @api private - class Slice < Command + class Slice < App::Command argument :name, required: true, desc: "The slice name" option :url, required: false, type: :string, desc: "The slice URL prefix" From fffd123b70ee10eb6602803bbd89c016ea944c83 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Fri, 12 Jul 2024 12:21:30 -0600 Subject: [PATCH 51/56] Use constants file --- lib/hanami/cli/generators/app/operation.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/hanami/cli/generators/app/operation.rb b/lib/hanami/cli/generators/app/operation.rb index 0c2be8e1..4923fb30 100644 --- a/lib/hanami/cli/generators/app/operation.rb +++ b/lib/hanami/cli/generators/app/operation.rb @@ -2,6 +2,7 @@ require "erb" require "dry/files" +require_relative "../constants" require_relative "../../errors" module Hanami @@ -11,10 +12,6 @@ module App # @since 2.2.0 # @api private class Operation - # @since 2.2.0 - # @api private - KEY_SEPARATOR = %r{\.|/} - # @since 2.2.0 # @api private def initialize(fs:, inflector:, out: $stdout) From 54dd28b318aad8b339e10e1385837dc63ddf1740 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Sat, 13 Jul 2024 19:11:14 -0600 Subject: [PATCH 52/56] Move class methods above initialize --- lib/hanami/cli/ruby_file_generator.rb | 30 +++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/hanami/cli/ruby_file_generator.rb b/lib/hanami/cli/ruby_file_generator.rb index 2e1582ff..7442794f 100644 --- a/lib/hanami/cli/ruby_file_generator.rb +++ b/lib/hanami/cli/ruby_file_generator.rb @@ -21,21 +21,7 @@ def initialize(source_code) end end - INDENT = " " - - def initialize( - class_name: nil, - parent_class: nil, - modules: [], - header: [], - body: [] - ) - @class_name = class_name - @parent_class = parent_class - @modules = modules - @header = header.any? ? (header + [""]) : [] - @body = body - end + INDENT = " " def self.class(class_name, **args) new(class_name: class_name, **args).to_s @@ -51,6 +37,20 @@ def self.module(*names, **args) new(modules: module_names, class_name: nil, parent_class: nil, **args).to_s end + def initialize( + class_name: nil, + parent_class: nil, + modules: [], + header: [], + body: [] + ) + @class_name = class_name + @parent_class = parent_class + @modules = modules + @header = header.any? ? (header + [""]) : [] + @body = body + end + def to_s definition = lines(modules).map { |line| "#{line}\n" }.join source_code = [header, definition].flatten.join("\n") From 303f5023d7810005821ca347e09ce531c3aec802 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Sat, 13 Jul 2024 19:13:13 -0600 Subject: [PATCH 53/56] Use constants file --- lib/hanami/cli/ruby_file_generator.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/hanami/cli/ruby_file_generator.rb b/lib/hanami/cli/ruby_file_generator.rb index 7442794f..86e0859d 100644 --- a/lib/hanami/cli/ruby_file_generator.rb +++ b/lib/hanami/cli/ruby_file_generator.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "ripper" +require_relative "generators/constants" module Hanami module CLI @@ -21,8 +22,6 @@ def initialize(source_code) end end - INDENT = " " - def self.class(class_name, **args) new(class_name: class_name, **args).to_s end @@ -107,7 +106,7 @@ def indent(line) if line.strip.empty? "" else - INDENT + line + INDENTATION + line end end From e824e5d1582e39bce3094de94048b4753d0d3e3b Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Sat, 13 Jul 2024 19:20:44 -0600 Subject: [PATCH 54/56] Add yard comments --- lib/hanami/cli/ruby_file_generator.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/hanami/cli/ruby_file_generator.rb b/lib/hanami/cli/ruby_file_generator.rb index 86e0859d..b96bb190 100644 --- a/lib/hanami/cli/ruby_file_generator.rb +++ b/lib/hanami/cli/ruby_file_generator.rb @@ -6,6 +6,8 @@ module Hanami module CLI class RubyFileGenerator + # @api private + # @since 2.2.0 class GeneratedUnparseableCodeError < Error def initialize(source_code) super( From 94c92e11b82d3c8247623824a97417a2c6d7f808 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Sat, 13 Jul 2024 19:29:27 -0600 Subject: [PATCH 55/56] Revert "Use constants file" This reverts commit 303f5023d7810005821ca347e09ce531c3aec802. Would need to namespace it and we may want to this to standalone so keeping it here. It's just two little spaces anyway --- lib/hanami/cli/ruby_file_generator.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/hanami/cli/ruby_file_generator.rb b/lib/hanami/cli/ruby_file_generator.rb index b96bb190..e9ac00c5 100644 --- a/lib/hanami/cli/ruby_file_generator.rb +++ b/lib/hanami/cli/ruby_file_generator.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "ripper" -require_relative "generators/constants" module Hanami module CLI @@ -24,6 +23,8 @@ def initialize(source_code) end end + INDENT = " " + def self.class(class_name, **args) new(class_name: class_name, **args).to_s end @@ -108,7 +109,7 @@ def indent(line) if line.strip.empty? "" else - INDENTATION + line + INDENT + line end end From 66d0c8021e5c14b64815541fa296bb531f86ed35 Mon Sep 17 00:00:00 2001 From: Sean Collins Date: Sat, 13 Jul 2024 19:34:28 -0600 Subject: [PATCH 56/56] Fix indent to be two spaces --- lib/hanami/cli/ruby_file_generator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/hanami/cli/ruby_file_generator.rb b/lib/hanami/cli/ruby_file_generator.rb index e9ac00c5..2652cf50 100644 --- a/lib/hanami/cli/ruby_file_generator.rb +++ b/lib/hanami/cli/ruby_file_generator.rb @@ -23,7 +23,7 @@ def initialize(source_code) end end - INDENT = " " + INDENT = " " def self.class(class_name, **args) new(class_name: class_name, **args).to_s