Skip to content

Commit

Permalink
Generate struct (#203)
Browse files Browse the repository at this point in the history
* Add dry-operation to default Gemfile

* Add base Operation class, based on dry-operation

* Fix view spec

* Add Operation generators

* Add empty `call` method definition

* Remove ostruct

* Allow slash separator for generator

* Allow slash separator for generator

* Rename module to admin

* Remove newlines in generated files

By adding new templates for un-nested operations

* Remove input as default args

* Remove Operations namespace, generate in app/ or slices/SLICE_NAME/

* Prevent generating operation without namespace

* Revert "Prevent generating operation without namespace"

This reverts commit a5bd2f3.

* Add recommendation to add namespace to operations

* Change examples

* Switch to outputting directly, remove Files#recommend

* Add Hanami::CLI::RubyFileGenerator

* x.x.x => 2.2.0

* x.x.x => 2.2.0

* Include Dry::Monads[:result] in base Action

* Add .module tests

* Convert top-level app operation to use RubyFileGenerator

* Convert nested app operation to use RubyFileGenerator

* Support slash separators

* Convert top-level slice operation to use RubyFileGenerator

* Remove OperationContext

* Remove namespaces instance variable

* Refactor to variables

* Remove last temporary instance variable

* Refactor

* More refactoring, for clarity

* Rename variable for clarity

* Rename helper method

* Simplify RubyFileGenerator, support older versions

* Convert Operation generator to use simplified RubyFileGenerator

* Remove un-used errors

* Refactor

* Older kwargs forwarding style

* Refactor

* Rename variable

* Add explanatory comment

Add dry-monads include for slice base action

* Fix base slice action

* Remove un-used ERB templates

* Remove OperationContext

* Ternary over and/or

* Fix missing 'end' from bad merge

* Fix namespace recommendation

* Extract App::Generate::Command

* Specify full name, to use App::Command

* Use constants file

* Move class methods above initialize

* Use constants file

* Add yard comments

* Revert "Use constants file"

This reverts commit 303f502.

Would need to namespace it and we may want to this to standalone so
keeping it here. It's just two little spaces anyway

* Fix indent to be two spaces

* Generate struct (with RubyFileGenerator, add RubyFileWriter) (#199)

* Add struct generator

* Extract App::Generate::Command

* Specify full name, to use App::Command

* Use App::Generate::Command for Struct

* Use KEY_SEPARATOR from constants file

* Provide generator class to command

* Extract Helper, use for Struct generator

* Fix specs with Helper

* kwargs

* Rename helper to RubyFileWriter

* Fix examples

* Remove optional kwarg

* Reorder args

* Rename to relative_parent_class

* Remove duplicate implementation

* Add api doc comments

* Reorder methods

* Refactor initialize

* Refactor to use method instead of arg

* Refactor to move logic into generator

* Reorder assignments

* Add Hanami::CLI::RubyFileGenerator, convert Operation to use it instead of ERB (#186)

* Add dry-operation to default Gemfile

* Add base Operation class, based on dry-operation

* Fix view spec

* Add Operation generators

* Add empty `call` method definition

* Remove ostruct

* Allow slash separator for generator

* Allow slash separator for generator

* Rename module to admin

* Remove newlines in generated files

By adding new templates for un-nested operations

* Remove input as default args

* Remove Operations namespace, generate in app/ or slices/SLICE_NAME/

* Prevent generating operation without namespace

* Revert "Prevent generating operation without namespace"

This reverts commit a5bd2f3.

* Add recommendation to add namespace to operations

* Change examples

* Switch to outputting directly, remove Files#recommend

* Add Hanami::CLI::RubyFileGenerator

* x.x.x => 2.2.0

* x.x.x => 2.2.0

* Include Dry::Monads[:result] in base Action

* Add .module tests

* Convert top-level app operation to use RubyFileGenerator

* Convert nested app operation to use RubyFileGenerator

* Support slash separators

* Convert top-level slice operation to use RubyFileGenerator

* Remove OperationContext

* Remove namespaces instance variable

* Refactor to variables

* Remove last temporary instance variable

* Refactor

* More refactoring, for clarity

* Rename variable for clarity

* Rename helper method

* Simplify RubyFileGenerator, support older versions

* Convert Operation generator to use simplified RubyFileGenerator

* Remove un-used errors

* Refactor

* Older kwargs forwarding style

* Refactor

* Rename variable

* Add explanatory comment

Add dry-monads include for slice base action

* Fix base slice action

* Remove un-used ERB templates

* Remove OperationContext

* Ternary over and/or

* Fix missing 'end' from bad merge

* Fix namespace recommendation

* Extract App::Generate::Command

* Specify full name, to use App::Command

* Use constants file

* Move class methods above initialize

* Use constants file

* Add yard comments

* Revert "Use constants file"

This reverts commit 303f502.

Would need to namespace it and we may want to this to standalone so
keeping it here. It's just two little spaces anyway

* Fix indent to be two spaces

* Remove extraneous requires

* Use out.string.chomp

* Fix name of expectation
  • Loading branch information
cllns authored Jul 14, 2024
1 parent d8837ec commit 0c8fdd6
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 84 deletions.
1 change: 1 addition & 0 deletions lib/hanami/cli/commands/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def self.extended(base)
prefix.register "operation", Generate::Operation
prefix.register "part", Generate::Part
prefix.register "slice", Generate::Slice
prefix.register "struct", Generate::Struct
prefix.register "view", Generate::View
end
end
Expand Down
10 changes: 6 additions & 4 deletions lib/hanami/cli/commands/app/generate/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@ class Command < App::Command
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)
super
@generator = generator_class.new(fs: fs, inflector: inflector, out: out)
end

def generator_class
# Must be implemented by subclasses, with class that takes:
# fs:, inflector:, out:
end

# @since 2.2.0
# @api private
def call(name:, slice: nil, **)
Expand Down
12 changes: 2 additions & 10 deletions lib/hanami/cli/commands/app/generate/operation.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
# frozen_string_literal: true

require "dry/inflector"
require "dry/files"
require "shellwords"
require_relative "../../../naming"
require_relative "../../../errors"

module Hanami
module CLI
module Commands
Expand All @@ -19,10 +13,8 @@ class Operation < Generate::Command
%(books.add --slice=admin (Admin::Books::Add)),
]

# @since 2.2.0
# @api private
def initialize(**opts)
super(generator_class: Generators::App::Operation, **opts)
def generator_class
Generators::App::Operation
end
end
end
Expand Down
28 changes: 28 additions & 0 deletions lib/hanami/cli/commands/app/generate/struct.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module Hanami
module CLI
module Commands
module App
module Generate
# @since 2.2.0
# @api private
class Struct < Command
argument :name, required: true, desc: "Struct name"
option :slice, required: false, desc: "Slice name"

example [
%(book (MyApp::Structs::Book)),
%(book/published_book (MyApp::Structs::Book::PublishedBook)),
%(book --slice=admin (Admin::Structs::Book)),
]

def generator_class
Generators::App::Struct
end
end
end
end
end
end
end
82 changes: 15 additions & 67 deletions lib/hanami/cli/generators/app/operation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,79 +23,27 @@ def initialize(fs:, inflector:, out: $stdout)
# @since 2.2.0
# @api private
def call(app_namespace, key, slice)
operation_name = key.split(KEY_SEPARATOR)[-1]
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 local_namespaces.none?

directory = directory(slice, local_namespaces: local_namespaces)
path = fs.join(directory, "#{operation_name}.rb")
fs.mkdir(directory)
RubyFileWriter.new(
fs: fs,
inflector: inflector,
app_namespace: app_namespace,
key: key,
slice: slice,
relative_parent_class: "Operation",
body: ["def call", "end"],
).call

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

private

attr_reader :fs, :inflector, :out

def directory(slice = nil, local_namespaces:)
base = if slice
fs.join("slices", slice)
else
fs.join("app")
end

if local_namespaces.any?
fs.join(base, local_namespaces)
else
fs.join(base)
end
end

def class_definition(operation_name:, container_namespace:, local_namespaces:)
container_module = normalize(container_namespace)

modules = local_namespaces
.map { normalize(_1) }
.compact
.prepend(container_module)

parent_class = [container_module, "Operation"].join("::")

RubyFileGenerator.class(
normalize(operation_name),
parent_class: parent_class,
modules: modules,
body: ["def call", "end"],
header: ["# frozen_string_literal: true"],
)
end

def normalize(name)
inflector.camelize(name).gsub(/[^\p{Alnum}]/, "")
end

def print_namespace_recommendation(operation_name)
out.puts(
" Note: We generated 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
Expand Down
149 changes: 149 additions & 0 deletions lib/hanami/cli/generators/app/ruby_file_writer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# frozen_string_literal: true

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

module Hanami
module CLI
module Generators
module App
# @since 2.2.0
# @api private
class RubyFileWriter
# @since 2.2.0
# @api private
def initialize(
fs:,
inflector:,
app_namespace:,
key:,
slice:,
relative_parent_class:,
extra_namespace: nil,
body: []
)
@fs = fs
@inflector = inflector
@app_namespace = app_namespace
@key = key
@slice = slice
@extra_namespace = extra_namespace&.downcase
@relative_parent_class = relative_parent_class
@body = body
raise_missing_slice_error_if_missing(slice) if slice
end

# @since 2.2.0
# @api private
def call
fs.mkdir(directory)
fs.write(path, file_contents)
end

private

# @since 2.2.0
# @api private
attr_reader(
:fs,
:inflector,
:app_namespace,
:key,
:slice,
:extra_namespace,
:relative_parent_class,
:body,
)

# @since 2.2.0
# @api private
def file_contents
class_definition(
class_name: class_name,
container_namespace: container_namespace,
local_namespaces: local_namespaces,
)
end

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

# @since 2.2.0
# @api private
def container_namespace
slice || app_namespace
end

# @since 2.2.0
# @api private
def local_namespaces
Array(extra_namespace) + key.split(KEY_SEPARATOR)[..-2]
end

# @since 2.2.0
# @api private
def directory
base = if slice
fs.join("slices", slice)
else
fs.join("app")
end

@directory ||= if local_namespaces.any?
fs.join(base, local_namespaces)
else
fs.join(base)
end
end

# @since 2.2.0
# @api private
def path
fs.join(directory, "#{class_name}.rb")
end

# @since 2.2.0
# @api private
def class_definition(class_name:, container_namespace:, local_namespaces:)
container_module = normalize(container_namespace)

modules = local_namespaces
.map { normalize(_1) }
.compact
.prepend(container_module)

parent_class = [container_module, relative_parent_class].join("::")

RubyFileGenerator.class(
normalize(class_name),
parent_class: parent_class,
modules: modules,
header: ["# frozen_string_literal: true"],
body: body
)
end

# @since 2.2.0
# @api private
def normalize(name)
inflector.camelize(name).gsub(/[^\p{Alnum}]/, "")
end

# @since 2.2.0
# @api private
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
end
end
44 changes: 44 additions & 0 deletions lib/hanami/cli/generators/app/struct.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

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

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

# @since 2.2.0
# @api private
def call(app_namespace, key, slice)
RubyFileWriter.new(
fs: fs,
inflector: inflector,
app_namespace: app_namespace,
key: key,
slice: slice,
extra_namespace: "Structs",
relative_parent_class: "DB::Struct",
).call
end

private

attr_reader :fs, :inflector, :out
end
end
end
end
end
5 changes: 2 additions & 3 deletions spec/unit/hanami/cli/commands/app/generate/operation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@
require "hanami"

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

let(:out) { StringIO.new }
let(:fs) { Hanami::CLI::Files.new(memory: true, out: out) }
let(:inflector) { Dry::Inflector.new }
let(:generator_class) { Hanami::CLI::Generators::App::Operation }
let(:app) { Hanami.app.namespace }
let(:dir) { inflector.underscore(app) }

def output
out.rewind && out.read.chomp
out.string.chomp
end

context "generating for app" do
Expand Down
Loading

0 comments on commit 0c8fdd6

Please sign in to comment.