2018-06-12

Hanamiコードリーディング【hanami new】

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を投げてみたのですが過去にもやりとりが有って通らなかったっぽいですね…
このエントリーをはてなブックマークに追加