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 nitoyon
Liquid はオブジェクトを表示するときには、必ず 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
クラスを使えば便利だよ、という話をした。