2018-02-16

hirbコードリーディング

hirb (0.7.3)のコードリーディングをしました。

irb, ripl, pryあたりに対応していますが、基本的にはHirb::View.view_or_page_output(output)のメソッドが呼び出され、outputのオブジェクトが整形されて出力されることになります。

今回はRailsで利用頻度が高いを思われるoutputにActiveRecord::BaseやActiveRrecord::Relationのインスタンスが入るケースを追ってみます。

Hirb::View.view_or_page_outputメソッドは以下のようにview_output経由でrender_outputを呼び出します。

module Hirb
  module View
    class<<self

      def view_or_page_output(str)
        view_output(str) || page_output(str.inspect, true)
      end

      def view_output(output, options={})
        enabled? && config[:formatter] && render_output(output, options)
# ...
      end

      def render_output(output, options={})
        if (formatted_output = formatter.format_output(output, options))
          render_method.call(formatted_output)
          true
        else
          false
        end
      end

      def formatter(reload=false)
        @formatter = reload || @formatter.nil? ? Formatter.new(config[:output]) : @formatter
      end

HirbFormatter#format_outputは整形された文字列を出力しています。

module Hirb
  class Formatter

    def format_output(output, options={}, &block)
      output_class = determine_output_class(output)
      options = parse_console_options(options) if options.delete(:console)
      options = Util.recursive_hash_merge(klass_config(output_class), options)
      _format_output(output, options, &block)
    end

#determine_output_classで出力対象のクラスを取得します。ActiveRecord::Relationの場合は配列化してから、最初の要素のクラスを取り出します

    self.to_a_classes = %w{Array Set ActiveRecord::Relation}

    #:stopdoc:
    def to_a_classes
      @to_a_classes ||= self.class.to_a_classes.map {|e| Util.any_const_get(e) }.compact
    end

    def determine_output_class(output)
      output.respond_to?(:to_a) && to_a_classes.any? {|e| output.is_a?(e) } ?
        Array(output)[0].class : output.class
    end

_format_outputでhelper_class.renderが呼び出されます。ActiveRecord::Baseの場合はHirb::Helpers::AutoTable.renderが呼ばれます。

module Hirb
  class Formatter

    def _format_output(output, options, &block)
      output = options[:output_method] ? (output.is_a?(Array) ?
        output.map {|e| call_output_method(options[:output_method], e) } :
        call_output_method(options[:output_method], output) ) : output
      args = [output]
      args << options[:options] if options[:options] && !options[:options].empty?
      if options[:method]
        send(options[:method],*args)
      elsif options[:class] && (helper_class = Helpers.helper_class(options[:class]))
        helper_class.render(*args, &block)
      elsif options[:output_method]
        output
      end
    end

Hirb::Helpers::AutoTable.renderはさらにHirb::Helpers::ObjectTable.renderを呼び出します。

class Hirb::Helpers::AutoTable < Hirb::Helpers::Table
  extend Hirb::DynamicView

  def self.render(output, options={})
    output = Array(output)
    (defaults = dynamic_options(output[0])) && (options = defaults.merge(options))
    klass = options.delete(:table_class) || (
      !(output[0].is_a?(Hash) || output[0].is_a?(Array)) ?
      Hirb::Helpers::ObjectTable : Hirb::Helpers::Table)
    klass.render(output, options)
  end
end

dynamic_optionsによってActiveRecordのカラム一覧がoptions[:fields]に格納されます。

module Hirb
  module DynamicView

    def dynamic_options(obj)
      view_methods.each do |meth|
        if obj.class.ancestors.map {|e| e.to_s }.include?(method_to_class(meth))
          begin
            return send(meth, obj)
          rescue
            raise "View failed to generate for '#{method_to_class(meth)}' "+
              "while in '#{meth}' with error:\n#{$!.message}"
          end
        end
      end
      nil
    end

呼び出されるメソッドは出力対象のクラスによって異なりますが、ActiveRecord::Baseの場合はHirb::Views::Railsのactive_record__base_viewが呼ばれます

module Hirb::Views::Rails #:nodoc:
  def active_record__base_view(obj)
    {:fields=>get_active_record_fields(obj)}
  end

  def get_active_record_fields(obj)
    fields = obj.class.column_names.map {|e| e.to_sym }
    # if query used select
    if obj.attributes.keys.compact.sort != obj.class.column_names.sort
      selected_columns = obj.attributes.keys.compact
      sorted_columns = obj.class.column_names.dup.delete_if {|e| !selected_columns.include?(e) }
      sorted_columns += (selected_columns - sorted_columns)
      fields = sorted_columns.map {|e| e.to_sym}
    end
    fields
  end
end

Hirb::DynamicView.add Hirb::Views::Rails, :helper=>:auto_table

Hirb::Helpers::ObjectTable.renderメソッドではitem_hashes変数にActiveRecord::BaseやActiveRecord::Relationがハッシュ化された配列が入ります。

class Hirb::Helpers::ObjectTable < Hirb::Helpers::Table
  def self.render(rows, options ={})
    options[:fields] ||= [:to_s]
    options[:headers] ||= {:to_s=>'value'} if options[:fields] == [:to_s]
    item_hashes = options[:fields].empty? ? [] : Array(rows).inject([]) {|t,item|
      t << options[:fields].inject({}) {|h,f| h[f] = item.__send__(f); h}
    }
    super(item_hashes, options)
  end
end

この変数を引数にHirb::Helpers::Table.renderでfieldsオプションに従ってテーブルのテキストを表示します。

Hirb::Helpers::Table.renderの処理はやや長いので割愛しますが、Hirb::Helpers::Tableだけでこういった処理ができます。

puts Hirb::Helpers::Table.render([{a:123,b:true}],{fields: [:a, :b]})
# +-----+------+
# | a   | b    |
# +-----+------+
# | 123 | true |
# +-----+------+

このように配列とフィールドを指定するだけで良い感じにASCIIなテーブルを出力してくれます。

このエントリーをはてなブックマークに追加