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なテーブルを出力してくれます。