Hanamiのコードリーディングをしました。バージョンは1.2.0です。今回は hanami new
のアプリケーションの初期化コマンドのコードを追ってみます
bin/hanamiはHanami::CLI#callを呼び出します
#!/usr/bin/env ruby
require 'bundler'
require 'hanami/cli/commands'
Bundler.require(:plugins) if File.exist?(ENV["BUNDLE_GEMFILE"] || "Gemfile")
Hanami::CLI.new(Hanami::CLI::Commands).call
Hanami::CLI#callは以下のように定義されています。コンストラクタでHanami::CLI::Commandsがregistryとして渡されます。
module Hanami
class CLI
def initialize(registry)
@commands = registry
end
def call(arguments: ARGV, out: $stdout)
result = commands.get(arguments)
if result.found?
command, args = parse(result, out)
result.before_callbacks.run(command, args)
command.call(args)
result.after_callbacks.run(command, args)
else
usage(result, out)
end
end
commandはHanami::CLI::Commands::NewのインスタンスなのでHanami::CLI::Commands#callが呼ばれます。
callは以下のように定義されています
module Hanami
class CLI
module Commands
class New < Command # rubocop:disable Metrics/ClassLength
def call(project:, **options)
project = Utils::String.underscore(project)
database_config = DatabaseConfig.new(options[:database], project)
test_framework = TestFramework.new(hanamirc, options[:test])
template_engine = TemplateEngine.new(hanamirc, options[:template])
options[:project] = project
context = Context.new(
#...
)
assert_project_name!(context)
directory = project_directory(project)
files.mkdir(directory)
Dir.chdir(directory) do
generate_application_templates(context)
generate_empty_directories(context)
generate_test_templates(context)
generate_sql_templates(context)
generate_git_templates(context)
init_git
generate_app(context)
end
end
プロジェクトディレクトリを作って、その中に移動してから各種ファイルを生成します。
generate_xxxの各メソッドにはHanami::CLI::Commands::Contextのオブジェクトを渡しています。これは各ファイル生成に必要なパラメータをまとめたOpenStructのクラスになります。ファイル生成はerbでレンダリングされますが、レンダリング時のコンテキストがこのオブジェクトになります。
module Hanami
module CLI
module Commands
class Context < OpenStruct
def initialize(data)
data = data.each_with_object({}) do |(k, v), result|
v = Utils::String.new(v) if v.is_a?(::String)
result[k] = v
end
super(data)
freeze
end
def with(data)
self.class.new(to_h.merge(data))
end
def binding
super
end
end
例えば、#generate_application_templatesでは以下のように#generate_fileを呼び出してファイルを生成します。
def generate_application_templates(context)
source = templates.find("hanamirc.erb")
destination = project.hanamirc(context)
generate_file(source, destination, context)
source = templates.find(".env.development.erb")
destination = project.env(context, "development")
generate_file(source, destination, context)
# ...
end
#generate_fileは#renderを使ってファイルの中身を生成して、Hanami::Utils::Files#writeで書き出します。
def render(path, context)
template = File.read(path)
renderer = Renderer.new
renderer.call(template, context.binding)
end
def generate_file(source, destination, context)
files.write(
destination,
render(source, context)
)
end
Hanami::Utils::Files#writeは以下のように定義されています。openの呼び出しはKernel#openではなく、Hanami::Utils::Files#openです。
module Hanami
module Utils
module Files # rubocop:disable Metrics/ModuleLength
def self.write(path, *content)
mkdir_p(path)
open(path, ::File::CREAT | ::File::WRONLY | ::File::TRUNC, *content)
end
def self.open(path, mode, *content)
::File.open(path, mode) do |file|
file.write(Array(content).flatten.join)
end
end
ERBによるレンダリングはHanami::CLI::Commands::Renderer#call内のERB#resultで行われます。
class Renderer
TRIM_MODE = "-".freeze
# ...
def call(template, context)
::ERB.new(template, nil, TRIM_MODE).result(context)
end
end
おまけ
ちなみにRailsのようにbundle exec hanami new .
というようにカレントディレクトリの中にHanamiを展開することは現段階ではできないようです。カレントディレクトリに展開するPRを投げてみたのですが過去にもやりとりが有って通らなかったっぽいですね…