DSL by JRuby at JavaOne2012 JVM language BoF #jt12_b101

21
階層化Javaプロパティ ファイルDSL作成 JRuby2012/04/04 akimatter 1245日木曜日

description

JavaOne 2012 JVM言語BoF プログラミング大会 - DSL 階層化JavaプロパティファイルDSL作成 JRuby編

Transcript of DSL by JRuby at JavaOne2012 JVM language BoF #jt12_b101

Page 1: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

階層化JavaプロパティファイルDSL作成 JRuby編2012/04/04 akimatter

12年4月5日木曜日

Page 2: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

自己紹介秋間武志

@akimatter

Java10年、Ruby6年

Ruby Business Commons運営委員

株式会社グルーヴノーツテクニカルプロデューサー/プログラマ

12年4月5日木曜日

Page 3: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

要件compiler.error.message.varNotFound=...compiler.error.message.incompatibleType=...compiler.error.maxReport=compiler.files.input.encoding=compiler.files.output.encoding=

compiler { error { message { varNotFound= incompatibleType= } } maxReport= files { input.encoding= output.encoding= }}

のように擬似的に階層的に書けると、より読みやすくなることが考えられる。このような、プロパティファイルのキーを階層構造で記述できるDSLを作成する事とする。

12年4月5日木曜日

Page 4: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

cascading_properties

名前重要

ライブラリ名 cascading_properties

モジュール名 CascadingProperties

12年4月5日木曜日

Page 5: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

ノードの名前はメソッド

アンダースコアがついているPropertiesのキーの一部となる部分は、Rubyではメソッドとして書ける。

compiler { error { message { varNotFound "variable not found" incompatibleType "incompatible type" } } maxReport 10 files { input.encoding "SJIS" output.encoding "UTF-8" }}

12年4月5日木曜日

Page 6: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

specはこんな感じ  valid_source1 = <<-EOS compiler { error { message { varNotFound "variable not found" incompatibleType "incompatible type" } } maxReport 10 files { input.encoding "SJIS" output.encoding "UTF-8" } } EOS

subject{ CascadingProperties.load(valid_source1) }

it "should export to java.util.Properties" do    props = subject.to_properties    props.should be_a(java.util.Properties)    props.size().should == 5    props.getProperty("compiler.error.message.varNotFound" ).should == "variable not found"    props.getProperty("compiler.error.message.incompatibleType").should == "incompatible type"    props.getProperty("compiler.maxReport").should == '10'    props.getProperty("compiler.files.input.encoding").should == 'SJIS'    props.getProperty("compiler.files.output.encoding").should == 'UTF-8'end

12年4月5日木曜日

Page 7: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

設計

12年4月5日木曜日

Page 8: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

オブジェクトの関連

compiler { error { message { varNotFound "variable not found" incompatibleType "incompatible type" } } maxReport 10 files { input.encoding "SJIS" output.encoding "UTF-8" }}

ツリー構造にマッピングできそう

12年4月5日木曜日

Page 9: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

Object#method_missingメソッド

オブジェクトに定義されていないメソッドが呼び出された際に呼び出されるフックメソッド。

呼び出されたメソッド名とそれに渡された引数とブロックが、method_missingの引数とブロックとして渡される。

12年4月5日木曜日

Page 10: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

メソッドのコンテキストcompiler {

}

error {

} maxReport 10 files {

}

message {

}

input.encoding "SJIS"output.encoding "UTF-8"

varNotFound "variable not found"incompatibleType "incompatible type"

メソッドはその外側のノードをselfとするコンテキストで呼び出される

12年4月5日木曜日

Page 11: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

BlankSlateObjectクラスには予めたくさんのメソッドが定義されているので、多くの語彙をmethod_missingで扱えるようにするためには、既存のメソッドができるだけ定義されていないクラスを使う。このようなクラスをBlankSlateと呼ぶ。

class CascadingProperties::BlankSlate  used = %w[instance_eval send class to_s hash inspect respond_to? should]  regexp = Regexp.new("/^__|^=|^\%|", used.map{|m| "^#{m}$"}.join('|') + '/')  instance_methods.each { |m| undef_method m unless m =~ regexp }end

定義時にused以外のメソッドをundef_methodして無かったことにしている。

12年4月5日木曜日

Page 12: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

クラスの構成

12年4月5日木曜日

Page 13: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

もっとも重要な部分

引数の数やブロックの有無で振る舞いが異なる

class CascadingProperties::Node < CascadingProperties::BlankSlate  def method_missing(name, *args, &block)    name = name.to_s    case args.length    when 0 then      @_hash[name] ||= CascadingProperties::Node.new(&block)    when 1 then      @_hash[name] = args.first    else      @_hash[name] = args    end  endend

12年4月5日木曜日

Page 14: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

java.util.Propetiesへの出力

class CascadingProperties::Node < CascadingProperties::BlankSlate  def to_properties(dest = nil, parent = nil)    dest ||= java.util.Properties.new    @_hash.each do |k, v|      key = parent ? "#{parent}.#{k}" : k      if v.respond_to?(:to_properties)        v.to_properties(dest, key)      else        dest.setProperty(key, v.to_s)      end    end    dest  endend

12年4月5日木曜日

Page 15: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

java.util.Propetiesへの出力

  def to_properties(dest = nil, parent = nil)    dest ||= java.util.Properties.new    @_hash.each do |k, v|      key = parent ? "#{parent}.#{k}" : k      if v.respond_to?(:to_properties)        v.to_properties(dest, key)      else        dest.setProperty(key, v.to_s)      end    end    dest  end

引数destはデフォルトではnil。jruby上で動く場合ならdestはjava.util.Propertiesを

生成する

12年4月5日木曜日

Page 16: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

java.util.Propetiesへの出力

  def to_properties(dest = nil, parent = nil)    dest ||= java.util.Properties.new    @_hash.each do |k, v|      key = parent ? "#{parent}.#{k}" : k      if v.respond_to?(:to_properties)        v.to_properties(dest, key)      else        dest.setProperty(key, v.to_s)      end    end    dest  end

値がto_properties可能ならそれを呼び出した結果を、そうでないのなら値を文字列として 、destに

setPropertyする

12年4月5日木曜日

Page 17: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

java.util.Propetiesも拡張できるrequire 'java'require 'cascading_properties'

java.util.Properties.instance_eval do  def load_dsl(dsl_text)    node = CascadingProperties.load(dsl_text)    node.to_properties  endend

12年4月5日木曜日

Page 18: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

おまけ

12年4月5日木曜日

Page 19: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

設定ファイルでERBを

ERBを使うことで、動的な値を設定ファイルに記述すること

も可能。

compiler {  error {    message {      varNotFound "variable not found"      incompatibleType "incompatible type"    }  }  maxReport <%= ENV['MAX_REPORT'] %>  files {    input.encoding "SJIS"    output.encoding "UTF-8"  }}

12年4月5日木曜日

Page 20: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

簡単にERBも使えるように    def load_file(filepath)      load(File.read(filepath))    end

    def load_file(filepath)      erb = ERB.new(IO.read(filepath))      erb.filename = filepath      text = erb.result      load(text)    end

12年4月5日木曜日

Page 21: DSL by JRuby at JavaOne2012  JVM language BoF #jt12_b101

ソースコード

https://github.com/akm/cascading_properties

rspecでテストも書いてあるよ

12年4月5日木曜日