Ruby の Liquid でテンプレートに値を渡すパターン4つ
今日は Ruby のテンプレート エンジン Liquid において、コードとテンプレートの間でデータをやり取りする方法についてまとめておく。
Liquid のバージョンは 2.3.0 で確認しているが、執筆時点で最新の 2.4.1 でも変わってないようにみえる。
Hash を渡すパターン
まずは、Liquid のサイトにも載ってる一番単純なハッシュを渡すパターン。
require 'liquid'
template = Liquid::Template.parse("hi {{name}}")
puts template.render( 'name' => 'nitoyon' ) # => hi nitoyon分かりやすい。そのまま。
to_liquid を実装するパターン
自分で作ったクラスのインスタンスを Liquid に渡したい場合もあるだろう。この場合、to_liquid メソッドを実装してやる必要がある。
require 'liquid'
class Person
def initialize
@name = "nitoyon"
end
def to_liquid
{ 'name' => @name }
end
end
template = Liquid::Template.parse("hi {{person.name}}")
puts template.render( 'person' => Person.new ) # => hi nitoyonLiquid はオブジェクトを表示するときには、必ず to_liquid メソッドを呼んでいる。to_liquid メソッドが定義されていない状態で表示しようとすると、
hi Liquid error: undefined method `to_liquid' for
#<Person:0x46a400 @name="nitoyon">
のようなエラーになってしまう。
この制約は String についても例外ではない。Liquid を require すると、裏側では liquid/extensions.rb によって String#to_liquid や Hash#to_liquid などのメソッドが定義されている。
to_liquid を独自実装するパターンを使うと、テンプレート側から Person のメソッドを呼ぶことはできない。それをやりたいなら、次に説明する Liquid::Drop を使うとよい。
Drop を使うパターン
Liquid::Drop を継承してやることで、テンプレート側からメソッドを呼べるようになる。
class Person < Liquid::Drop
attr_accessor :name
def initialize
@name = "nitoyon"
end
def NAME
@name.upcase
end
end
template = Liquid::Template.parse("hi {{person.name}}")
puts template.render({'person' => Person.new }) # => hi nitoyon
template = Liquid::Template.parse("hi {{person.NAME}}")
puts template.render({'person' => Person.new }) # => hi NITOYONこれはとても便利だが、いくつか注意すべき点がある。
まず、引数付きのメソッドを呼ぶことはできない。引数に対処したかったら、テンプレートから name_param1_param2 のようにして参照しておいて、後述の before_method でがんばってパースしろ、というポリシーのようだ。
もう1つ。別ライブラリが提供するオブジェクトを Liquid に渡したいことがある。このとき、Liquid::Drop を継承させるのは不可能だ。かといって、to_liquid で内部構造をいちいちハッシュに変換するのも面倒だ。
そんなケースに対処するために Drop 化させるクラス ToDrop を作ってみたので、次のパターンとして紹介する。
Drop 化させるパターン
gem かなんかで、こんなクラスが提供されているものと仮定する。
module Foo
class Person
attr_accessor :name
def initialize
@name = "nitoyon"
end
def NAME
@name.upcase
end
end
end外部ライブラリーのクラスなのでいじりたくないけど、このクラスのインスタンスをテンプレートに渡して、 name や NAME メソッドを叩きたいものとする。
そういうときは、次のような ToDrop クラスを定義しておけばよい。
class ToDrop < Liquid::Drop
def initialize(obj)
@obj = obj
end
def before_method(method)
if method && method != '' && @obj.class.public_method_defined?(method.to_s.to_sym)
@obj.send(method.to_s.to_sym)
end
end
end使い方は簡単。to_liquid で ToDrop.new(self) を返す処理を実装してやるだけだ。これだけで期待の動作となっている。
module Foo
class Person
def to_liquid
ToDrop.new(self)
end
end
end
template = Liquid::Template.parse("hi {{person.name}}")
puts template.render({'person' => Foo::Person.new }) # => hi nitoyon
template = Liquid::Template.parse("hi {{person.NAME}}")
puts template.render({'person' => Foo::Person.new }) # => hi NITOYONこの ToDrop が今回、わたしが作成した魔法のクラスで、任意のオブジェクトを Liquid::Drop を継承したときと同じ動作にしてくれる。
ToLiquid クラスでは Liquid::Drop#before_method メソッドを実装している。このメソッドは Drop のメソッドミッシングのような役割を担っている。ToDrop#before_method では、ラップ対象のオブジェクトに public メソッドがあるかどうか調べて、あるならばそれを呼ぶよう実装している。
言葉で説明しても分かりにくいのだけど、Liquid::Drop クラスのソース drop.rb と見比べてもらうとイメージは沸きやすいと思う。Liquid::Drop のソースをよく見ると Liquid と密接に関わっているわけではなく、alias :[] :invoke_drop とすることで、[] を使った参照をメソッド呼び出しに置き換えているだけ、というヘルパークラスなのが興味深い。
まとめ
Liquid に値を渡すためのパターンを 4 つ紹介した。Liquid::Drop を継承すればテンプレート側からメソッドを呼べるし、継承関係に手が出しにくいときには拙作の ToDrop クラスを使えば便利だよ、という話をした。