basyura's blog

あしたになったらほんきだす。

Ruby - attr_accessor

今更だけど attr_accessor で気持ち悪いところ。

class Hoge
  attr_accessor :attr1, :attr2, :attr3

  def initialize(map)
    attr1 = map[:attr1]
    @attr2 = map[:attr2]
    self.attr3 = map[:attr3]
  end

  def to_map
    { attr1: attr1, attr2: @attr2, attr3: self.attr3 }
  end
end

hoge = Hoge.new(attr1: "one", attr2: "two", attr3: "three")

p hoge.attr1 #=> nil
p hoge.attr2 #=> "two"
p hoge.attr3 #=> "three"

p hoge.to_map #=> {:attr1=>nil, :attr2=>"two", :attr3=>"three"}

attr_accessor で定義しているのだから attr1 = の代入はできて良さそうなのだけどできない。でも attr1 で取得することはできる。地味にハマることがある。というほど書いてないのだけどたまに書くとどうだったっけなと確認が必要になる。クラス内で扱うときは素直に @ 使えと覚えておけば良いのかな。

https://docs.ruby-lang.org/ja/2.5.0/method/Module/i/attr_accessor.html
attr_accessor(*name) -> nil[permalink][rdoc]
インスタンス変数 name に対する読み取りメソッドと書き込みメソッドの両方を 定義します。

このメソッドで定義されるメソッドの定義は以下の通りです。

def name
@name
end
def name=(val)
@name = val
end

[PARAM] name:
String または Symbol を 1 つ以上指定します。

マニュアルもできそうに読めるのだけどな。

MacVim + Ruby 2.3

rbenv に乗り換えて ruby 2.3 を入れたまでは良かったけど vim から見える ruby がシステムデフォルトの 2.0 になってた。rvm を使っていたときは DYNAMIC_RUBY_DLL か rubydll で libruby.*.dylib を指定していたように思うけど設定ファイルを消失してしまって思い出せない。dropbox の履歴が去年の 11 月までしか見れず、"旧バージョンを表示" リンクを選んでも直近の履歴が表示されてループする (バグ?)。.rbenv 配下に dylib が無いようなので vimrc でパスを通したら動いた。

let $PATH = $HOME. '/.rbenv/shims:' .  $PATH
let $GEM_PATH = $HOME . '/.rbenv/versions/2.3.3/lib/ruby/gems/2.3.0'

set rubydll すると rubydll=libruby.2.0.0.dylib が出力されるけど、puts RUBY_VERSION が 2.3.3 って出るし gem も読んでくれてるのでとりあえず ok かな。

redmine の wiki が更新された時に diff も飛ばしたい

メールのテンプレートをいじればいいはずなんだけど。

  • redmine/app/views/wiki/diff.html.erb
<%= l(:mail_body_wiki_content_updated, :id => h(@wiki_content.page.pretty_title),
                                       :author => h(@wiki_content.author)) %>
<%= @wiki_content.comments %>

<%= @wiki_content.page.pretty_title %>:
<%= @wiki_content_url %>
<%= l(:label_view_diff) %>:
<%= @wiki_diff_url %>

↑ここに突っ込めばいいと思うのだけど、diff の作り方が分からない・・・。

page = @wiki_content.page
page.diff #=> WikiDiff

page.diff.content_from
page.diff.content_to

page.diff.to_html

こんな感じで情報は揃ってるんだけど、html じゃなくて unified 形式で欲しい・・・。
疲れたので今日はここまで。

Ripper → S 式 → XML

Rubykaigi で何回か登場した Ruby プログラムのパーサである Ripper。
web を徘徊してみたけど使い方がよくわからないのでゴニョゴニョしてみた。

基本的なクラスをパース

クラス定義

contents =<<-EOF
  class Hoge
    def say
    end
  end
EOF

Ripper で s 式に変える

Ripper でパース

require 'ripper'
require 'pp'
require 'rexml/document'
require 'stringio'

source = Ripper.sexp(contents)
pp source

出力結果

[:program,
 [[:class,
   [:const_ref, [:@const, "Hoge", [1, 8]]],
   nil,
   [:bodystmt,
    [[:def,
      [:@ident, "say", [2, 8]],
      [:params, nil, nil, nil, nil, nil, nil, nil],
      [:bodystmt, [[:void_stmt]], nil, nil, nil]]],
    nil,
    nil,
    nil]]]]

s 式を xml に変換する

ごりごりとノードを作って xml に変換していく。@ のイベントは属性にしたかったけど、複数登場する場合があるので子ノードとして追加するようにした。

def analyze(source, node = nil)
  node ||= REXML::Document.new
  return node unless source
  if source.first.kind_of?(Symbol) && source.first.to_s =~ /^@/
    node.add_element(source.first.to_s.sub('@','')).text = source[1]
  elsif source.first.kind_of? Symbol 
    new_node = node.add_element(source.first.to_s)
    for i in 1...source.length
      analyze(source[i], new_node)
    end
  elsif source.kind_of? Array
    source.each {|v| analyze(v, node) }
  end
  node
end

doc = analyze(source)

output = StringIO.new
formatter = REXML::Formatters::Pretty.new
formatter.compact = true
formatter.write(doc, output)

puts output.string

出力結果

<program>
  <class>
    <const_ref>
      <const>Hoge</const>
    </const_ref>
    <bodystmt>
      <def>
        <ident>say</ident>
        <params/>
        <bodystmt>
          <void_stmt/>
        </bodystmt>
      </def>
    </bodystmt>
  </class>
</program>

xml に変換できたので、xpath を使って定義を抜き出す

xpath って途中省略とかできた気がするんだけど、思うように取り出せないのでパスをゴリ書き。

ndoc = REXML::Document.new(output.string)
classes = ndoc.get_elements('//class')
classes.each do |clazz|
  print clazz.elements['const_ref/const'].text
  parent = clazz.elements['var_ref/const']
  puts parent ? ' < ' + parent.text : ''

  include = clazz.elements['bodystmt/command/ident']
  if include && include.text == 'include'
    print "\t include - "
    puts  include.parent.get_elements('args_add_block/var_ref/const').map {|v| v.text }.join(', ')
  end

  clazz.get_elements('bodystmt/def').each do |m|
     print "\t "  + m.elements['ident'].text
     params =  m.get_elements('paren/params/ident')
     puts params.length > 0 ? ' - ' + params.map {|v| v.text }.join(', ') : ' - void'
  end
end

出力結果

Hoge
	 say - void

もうちょっと複雑なクラスをパースしてみる

複数のクラスで継承しててメソッドも複数。

contents =<<-EOF
  class Hoge < Fuga
    include HogeModule, FugaModule    
    def initialize(name, age)
      name
    end
    def say
    end
  end
  class Fuga
    def foo
    end
  end
EOF

S 式にパースする

[:program,
 [[:class,
   [:const_ref, [:@const, "Hoge", [1, 8]]],
   [:var_ref, [:@const, "Fuga", [1, 15]]],
   [:bodystmt,
    [[:command,
      [:@ident, "include", [2, 4]],
      [:args_add_block,
       [[:var_ref, [:@const, "HogeModule", [2, 12]]],
        [:var_ref, [:@const, "FugaModule", [2, 24]]]],
       false]],
     [:def,
      [:@ident, "initialize", [3, 8]],
      [:paren,
       [:params,
        [[:@ident, "name", [3, 19]], [:@ident, "age", [3, 25]]],
        nil,
        nil,
        nil,
        nil,
        nil,
        nil]],
      [:bodystmt, [[:var_ref, [:@ident, "name", [4, 6]]]], nil, nil, nil]],
     [:def,
      [:@ident, "say", [6, 8]],
      [:params, nil, nil, nil, nil, nil, nil, nil],
      [:bodystmt, [[:void_stmt]], nil, nil, nil]]],
    nil,
    nil,
    nil]],
  [:class,
   [:const_ref, [:@const, "Fuga", [9, 8]]],
   nil,
   [:bodystmt,
    [[:def,
      [:@ident, "foo", [10, 8]],
      [:params, nil, nil, nil, nil, nil, nil, nil],
      [:bodystmt, [[:void_stmt]], nil, nil, nil]]],
    nil,
    nil,
    nil]]]]

xml に変換する

<program>
  <class>
    <const_ref>
      <const>Hoge</const>
    </const_ref>
    <var_ref>
      <const>Fuga</const>
    </var_ref>
    <bodystmt>
      <command>
        <ident>include</ident>
        <args_add_block>
          <var_ref>
            <const>HogeModule</const>
          </var_ref>
          <var_ref>
            <const>FugaModule</const>
          </var_ref>
        </args_add_block>
      </command>
      <def>
        <ident>initialize</ident>
        <paren>
          <params>
            <ident>name</ident>
            <ident>age</ident>
          </params>
        </paren>
        <bodystmt>
          <var_ref>
            <ident>name</ident>
          </var_ref>
        </bodystmt>
      </def>
      <def>
        <ident>say</ident>
        <params/>
        <bodystmt>
          <void_stmt/>
        </bodystmt>
      </def>
    </bodystmt>
  </class>
  <class>
    <const_ref>
      <const>Fuga</const>
    </const_ref>
    <bodystmt>
      <def>
        <ident>foo</ident>
        <params/>
        <bodystmt>
          <void_stmt/>
        </bodystmt>
      </def>
    </bodystmt>
  </class>
</program>

xpath で定義を抜き出す

Hoge < Fuga
	 include - HogeModule, FugaModule
	 initialize - name, age
	 say - void
Fuga
	 foo - void

vim での補完に利用したい

と、思ったけど対象の変数の型が分からんことにはやっぱり・・・ということで Ruby 3.0 に期待。

久しぶりに Ruby 書いたけど、リファレンスをあちこち見ながらなじゃないと書けなくなってる。「こんなメソッド無かったっけ?どう使えばいいんだっけ?」 → 「ひたすらググる」の繰り返しだったのだけど、こういう時に qiita を利用すればいいのかと思うなど。ただ、あそこに書くと完全に埋もれてしまう感(誰が書いてるかはどうでもいい的な)があるんだよなぁ。書いたこと無いけど。

Bitnami の Redmine で thin のインスタンスを増やす

redmineThin3 を追加する場合の手順メモ

サービスを登録

  • C:/Bitnami/redmine-2.5.0-0/apps/redmine/scripts/serviceinstall.bat

redmineThin1 を参考にして管理者権限で実行

"C:\Bitnami\redmine-2.5.0-0/apps/redmine\scripts\winserv.exe" install "redmineThin3" -start auto "C:\Bitnami\redmine-2.5.0-0\ruby\bin\ruby.exe" "C:\Bitnami\redmine-2.5.0-0/apps/redmine\htdocs\bin\thin" start -p 3003 -e production -c "C:\Bitnami\redmine-2.5.0-0/apps/redmine/htdocs" -a 127.0.0.1 --prefix /redmine

サービスを起動

net start redmineThin3 >NUL

Application Manager にサービスが表示されるように追加

  • C:/Bitnami/redmine-2.5.0-0/properties.ini
[Thin_redmine3]
thin_server_port=3003
thin_unique_service_name=redmineThin3

サービス起動停止の追加

  • C:/Bitnami/redmine-2.5.0-0/apps/redmine/scripts/servicerun.bat

追記

net start redmineThin3

net stop redmineThin3

apache で振り分けられるように設定

  • C:/Bitnami/redmine-2.5.0-0/apps/redmine/conf/httpd-prefix.conf
<Proxy balancer://redminecluster>
  BalancerMember http://127.0.0.1:3001/redmine

  BalancerMember http://127.0.0.1:3002/redmine

  BalancerMember http://127.0.0.1:3003/redmine
</Proxy>

まとめ

かなりめんどくさい。しばしば応答がなくなることがあったのでインスタンス数を増やしてみたのだけど windows の妙なディスクアクセスによるプチフリーズの方が原因っぽい。サービスを落としたり設定を変えたりして様子見中。今日はかなり快適に動いてた。

redmine_wiki_index_tree_view に keyword filter を追加

f:id:basyura:20140429220347p:plain

keyword に入力した内容でツリーを維持しながらフィルタできる機能を追加。
単純に onkeyup で要素をグルグルグルグルしながらフィルタしてるので数が多い場合は遅くなるかも。

jQuery 便利なんだけど、null じゃなくて size で存在判定とか、text() と思いきや val() だったとか地味にハマることがあるのがやや辛い。

Redmine の wiki index ページを tree view で表示する - redmine_wiki_index_tree_view

書いてみた

redmine デフォルトの表示。

f:id:basyura:20140429000727p:plain

tree view 化して第一階層だけ開く (デフォルト)

f:id:basyura:20140428235403p:plain

tree view 化して第二階層まで開く (js のコメントアウトをはずす)

f:id:basyura:20140428235402p:plain

wiki page が増えてくるとどこに何があるのか分からなくなってきて探すのが大変だし整理するのも大変。最初にページを全て表示するんじゃなくて階層化しておけばたどりやすくなるんじゃないかなぁと思って。ページ名をインクリメンタルサーチしつつツリー構造を維持するってとこまでやりたいんだけど・・・jquery のプラグイン転がってないかなぁ。

中身

たいしたことはしていないのだけど。

  • redmine_wiki_index_tree_view/lib/redmine_wiki_index_tree_view.rb
module Redmine
  module Plugins
    class RedmineWikiIndexTreeViewListener < Redmine::Hook::ViewListener
      def view_layouts_base_html_head(context)
        return unless context[:controller]
        params = context[:controller].params
        return unless (params[:controller] == 'wiki' && params[:action] == 'index')

        tags = [stylesheet_link_tag('jquery.treeview.css',
                                   :plugin => 'redmine_wiki_index_tree_view')]
        tags << javascript_include_tag('jquery.treeview.js',
                                   :plugin => 'redmine_wiki_index_tree_view')
        tags << javascript_include_tag('redmine_wiki_index_tree_view.js',
                                   :plugin => 'redmine_wiki_index_tree_view')
        tags.join("\n")
      end
    end
  end
end
  • redmine_wiki_index_tree_view/assets/javascripts/redmine_wiki_index_tree_view.js
$(document).ready(function(){
  $(".pages-hierarchy:first").treeview({
    collapsed: true,
  });
  $(".hitarea:first").click();
  // open second hierarchy
  //$('.treeview:first').find('ul:first').children('li').find('.hitarea:first').click();
});

Hook を使って jquery-treeview を埋め込んで、階層をクリックしてるだけ。

ライセンス

jquery-treeview が MIT/GPL のデュアルライセンスでどうしていいのかよく分からないのだけど、MIT LICENSE にして AUTHOR.txt に記載しておいた。あってるのかこれ・・・。

まとめ

plugin 作るの楽しいなぁ。