男女比はカレーと福神漬けと同じくらい

マサカリよろしくお願いします。

多対多の相手レコード数の最小を指定するバリデーション

こんなクラス構造

class Hoge < ApplicationRecord
  has_many :hoge_foos
  has_many :foos, through: :hoge_foos
  accepts_nested_attributes_for :hoge_foos
end
class Foo < ApplicationRecord
  has_many :hoge_foos
  has_many :hoges, through: :hoge_foos
end
class HogeFoo < ApplicationRecord
  belongs_to :hoge
  belongs_to :foo
end

 

やりたいこと

  • hogeのフォームからfooをチェックボックスで複数選択して紐付かせる
  • fooを少なくとも1つは選択してほしい

 

結論

これでいける

class Hoge < ApplicationRecord
    -- 略 --
    validate :validate_foos

  def validate_foos
    errors.add(:foos, 'を1つ以上選択してください') if foos.size.zero?
  end
end
  • validates を使ってできないものなのか
  • できるだけ命名したくない

探したらあった!…だけど

class Hoge < ApplicationRecord
    --- 略 ---
    validates :hoge_foos, 
        length: { 
            minimum: 1,
            message: 'を1つ以上選択してください' 
        }
end

http://stackoverflow.com/questions/4486749/rails-has-many-minimum-collection-size-update-validation

  • イケてる気がする!
  • エラーメッセージが「hoge_foosを1つ以上選択してください」
  • なんか違う

対応策を考える

i18nをいじってhoge_foosの表記を変える
ja:
    activerecord:
        attributes:
            hoge:
                hoge_foos: 'foos'

これはやったらダメな気がする


i18n%{attribute} を非表示にする
ja:
    errors:
    format: "%{message}"

全てのバリデーションのattributeが消えるため被害範囲が大きいのでダメ
Rails3のときはattributeを表示させなくするgemがあったらしい

 

まとめ

  • レールに乗っかるためにはカスタムバリデーションメソッドを作るのが一番良さそう

 

RubyとSwiftにおける感嘆符/疑問符の扱いの違い

RubyとSwiftでは「!」と「?」が多用されますが使い方が違います。
Swiftでの使い方に中々慣れなかったので自分なりに対比させてまとめてみました。

元々Rubyを書いていてこれからSwiftを書き始める人やその逆の人の参考になればと思います。

 

最も違うところ

Rubyメソッド名に「!」,「?」を使う

Swiftは型名, 変数名に「!」,「?」を使う

 

Rubyはメソッド名に「!」,「?」を使う

Rubyではメソッドの処理内容をメソッド名から類推しやすくするために「!」「?」を使います。

Rubyリファレンスには以下のように記載されています。

「!」はメソッド名の一部です。慣用的に、 同名の(! の無い)メソッドに比べてより破壊的な作用をもつメソッド(例: tr と tr!)で使われます。
 
「?」はメソッド名の一部分です。 慣用的に、真偽値を返すタイプのメソッドを示すために使われます。

 

具体例を示します。

# 破壊的なメソッド
a = "string"
b = a
a.upcase!
p a   # => "STRING"
p b   # => “STRING"

# 非破壊的なメソッド
a = "string"
b = a
a = a.upcase
p a   # => "STRING"
p b   # => “string”

# 真偽を返す
“”.empty? #=> true

rubyのメソッド名で「!」「?」はアルファベットと同じ扱いをします。
なのでupcaseupcase!のような「!」「?」がついているメソッドとついていないメソッドは似た処理を行う別のメソッドになります。 同様の理由で非破壊メソッドのhogeメソッドしか定義してないときにhoge!を呼び出してもhogeの処理に似た破壊的メソッドが実行されるわけではありません(当たり前ですが)。

"メソッドの処理内容をメソッド名から類推しやすくする”ための機能なのでmustではありません。shouldです。 「!」がついていないけど破壊的な変更を行うメソッドや「?」がついていないけど真偽値を返すメソッドを定義してもエラーを吐かれることはありません。

 

Swiftは型名,変数名に「!」,「?」を使う

swiftでは変数の値がnilである状態を許容するかどうかを明確に区別するために「!」「?」を使います。
nilを許容するかどうかを明確に判断することによりnil関連のエラーに出会う可能性を極力減らすことができます。

具体例を示します。

# nilである状態を許容しない
var x = 3
x = nil     # error: nil cannot be assigned to type ‘Int’
print(x)    # 上の行でエラーしているので実行できない

var y: Int = 3
y = nil     # error: nil cannot be assigned to type ‘Int’
print(y)    # 上の行でエラーしているので実行できない


# nilである状態を許容する
var z: Int? = 3
z = nil
print(z)    # => nil

# -----------------

#Int型の変数にInt?型の値を代入する(unwrapしない)
var a: Int = 5
var b:Int? = 10
var c:Int? = nil
a = b       # error: value of optional type 'Int?' not unwrapped; did you mean to use '!' or '?'?
print(a)    # 上の行でエラーしているので実行できない
a = c       # error: value of optional type 'Int?' not unwrapped; did you mean to use '!' or '?'?
print(a)    # 上の行でエラーしているので実行できない

#Int型の変数にInt?型の値を代入する(unwrapする)
var d:Int = 5
var e:Int? = 10
var f:Int? = nil
d = e!
print(d)    # => 10
d = f!      # fatal error: unexpectedly found nil while unwrapping an Optional value
print(d)    # => 10 (d = e!の結果が残っている)

Intの変数にInt?の変数を直接代入することはできません。
それはIntInt?が別の型でありInt?の変数の値がnilの可能性があるためです。
変数名の後ろに「!」をつけることにより値がnilでないことを明示する(アンラップする)ことができます。 そしてアンラップした変数は「?」のついていない型に代入できます。

Swiftでの「!」「?」はmustです。
正しくラップ/アンラップしなければコンパイルエラーを吐きます。    

Swiftの「!」「?」はまだ理解しきれていない部分があるので代表的なところだけを紹介しました。
この記事がかなり詳しいです。
[Swift] Optional 型についてのまとめ Ver2 - Qiita

`rails new my_app`を解読してみた

Railsアプリを作成するときに毎回使う rails new my_appで何が起きているか知りたかったのでソースを読み解いてみました。

初学者なので解説が間違っている, 解説の粒度がバラバラで読みにくい可能性があります。
何かありましたらコメントください。
コードは時系列で見ていくのでファイルを行ったり来たりしています。

設定

  • .railsrcは作成していない
  • 開発用PCはmac

概要

(1) bundle install railsRailsをインストールする
(2) bundle exec rails new my_appがユーザーによって実行される。
(3) Rails::AppRailsLoader#exec_app_railsが実行される。
(4) Rails::CommandsTasks#newを実行する。
(5) Rails::Generators::AppGenerator#startを実行する。
(6)Rails::Generators::AppGenerator#invoke_allを実行する。
(7) Thor::Command#runを実行する。
(8) Rails::Generators::AppBase#buildを実行する。

コマンドの実行

まずはユーザーがbundle install railsRailsをインストールする。

次にbundle exec rails new my_appを実行する。
bundlerがどんな挙動をしてるのかよく分からないけど今回はrails newの解読なのでとりあえず放置。

railsコマンドはrailties/bin/railsにあった。

#railties/bin/rails

#!/usr/bin/env ruby

git_path = File.expand_path('../../../.git', __FILE__)

if File.exist?(git_path)
  railties_path = File.expand_path('../../lib', __FILE__)
  $:.unshift(railties_path)
end
require "rails/cli"
  • $:$LOAD_PATHのショートカットらしい。
  • railties/lib$LOAD_PATHに追加してrails/cliをrequireしている。

 

require "rails/cli"を追いかける。

#railties/lib/rails/cli.rb

require 'rails/app_rails_loader'

# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
Rails::AppRailsLoader.exec_app_rails

require 'rails/ruby_version_check'
Signal.trap("INT") { puts; exit(1) }

if ARGV.first == 'plugin'
  ARGV.shift
  require 'rails/commands/plugin'
else
  require 'rails/commands/application'
end
  • #exec_app_railsの内部でexecが実行されるため実行に成功すると処理が戻ってこないため以降は実行されない。

 

Rails::AppRailsLoader.exec_app_railsを追いかける。

require 'pathname'

module Rails
  module AppRailsLoader # :nodoc:
    extend self

    RUBY = Gem.ruby
    EXECUTABLES = ['bin/rails', 'script/rails']
    BUNDLER_WARNING = <<EOS
Looks like your app's ./bin/rails is a stub that was generated by Bundler.

In Rails 4, your app's bin/ directory contains executables that are versioned
like any other source code, rather than stubs that are generated on demand.

Here's how to upgrade:

  bundle config --delete bin    # Turn off Bundler's stub generator
  rake rails:update:bin         # Use the new Rails 4 executables
  git add bin                   # Add bin/ to source control

You may need to remove bin/ from your .gitignore as well.

When you install a gem whose executable you want to use in your app,
generate it and add it to source control:

  bundle binstubs some-gem-name
  git add bin/new-executable

EOS

    def exec_app_rails
      original_cwd = Dir.pwd
      # original_cwd => カレントディレクトリの絶対パス

      loop do
        if exe = find_executable
          # exe => 'bin/rails'
          contents = File.read(exe)
          # contents =>
          #   APP_PATH = File.expand_path('../../config/application', __FILE__)
          #   require_relative '../config/boot'
          #   require 'rails/commands'

          if contents =~ /(APP|ENGINE)_PATH/
            exec RUBY, exe, *ARGV
            break # non reachable, hack to be able to stub exec in the test suite
          elsif exe.end_with?('bin/rails') && contents.include?('This file was generated by Bundler')
            $stderr.puts(BUNDLER_WARNING)
            Object.const_set(:APP_PATH, File.expand_path('config/application', Dir.pwd))
            require File.expand_path('../boot', APP_PATH)
            require 'rails/commands'
            break
          end
        end

        # If we exhaust the search there is no executable, this could be a
        # call to generate a new application, so restore the original cwd.
        Dir.chdir(original_cwd) and return if Pathname.new(Dir.pwd).root?

        # Otherwise keep moving upwards in search of an executable.
        Dir.chdir('..')
      end
    end

    def find_executable
      EXECUTABLES.find { |exe| File.file?(exe) }
    end
  end
end
  • contentsにはrailties/lib/rails/generators/rails/app/templates/bin/railsのソースが読み込まれている。
  • #exec_app_railsではrailties/lib/rails/generators/rails/app/templates/bin/railsARGVを引数として実行している。

 

exec RUBY, exe, *ARGVを追いかける。
exec RUBY, exe, *ARGV => % ruby railties/lib/rails/generators/rails/app/templates/bin/rails new my_app

#railties/lib/rails/generators/rails/app/templates/bin/rails

APP_PATH = File.expand_path('../../config/application', __FILE__)
require_relative '../config/boot'
require 'rails/commands'
  • ../config/bootrails/commandsをrequireしている。

 

require_relative '../config/boot'を追いかける。

#railties/lib/rails/generators/rails/app/templates/config/boot.rb

ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)

require 'bundler/setup' # Set up gems listed in the Gemfile.
  • デフォルトでGemfileに記述されているrubygemをrequireしている。

 

require 'rails/commands'を追いかける。

#railties/lib/rails/commands.rb

# ARGV => ['new', 'my_app']
ARGV << '--help' if ARGV.empty?  

aliases = {
  "g"  => "generate",
  "d"  => "destroy",
  "c"  => "console",
  "s"  => "server",
  "db" => "dbconsole",
  "r"  => "runner"
}

command = ARGV.shift
command = aliases[command] || command
# command => 'new'

require 'rails/commands/commands_tasks'

Rails::CommandsTasks.new(ARGV).run_command!(command)
  • railsコマンドの引数を渡して生成したRails::CommandsTasksインスタンス'new'を引数にrun_command!メソッドを実行している。
  • 引数を与えなかったらhelpが表示される理由が分かった。

 

Rails::CommandsTasks.new(ARGV).run_command!(command)を追いかける。

#railties/lib/rails/commands/commands_tasks.rb

module Rails
  class CommandsTasks # :nodoc:
    attr_reader :argv

    # ...   

    COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole runner new version help)

    def initialize(argv)
      @argv = argv
    end

    def run_command!(command)
      command = parse_command(command)
      # command => 'new'
      if COMMAND_WHITELIST.include?(command)
        send(command)
      else
        write_error_message(command)
      end
    end
    
    # ...

    def new
      # argv.first => 'new'
      if %w(-h --help).include?(argv.first)
        require_command!("application")
      else
        exit_with_initialization_warning!
      end
    end

    private

      # ...

      def require_command!(command)
        require "rails/commands/#{command}"
      end

      # ...

      def parse_command(command)
        case command
        when '--version', '-v'
          'version'
        when '--help', '-h'
          'help'
        else
          command
        end
      end
end
  • #run_command!ではcommand#parse_commandを通した後COMMAND_WHITELISTに存在するか確かめて#newを実行している。
  • #newでは'new','-h''--help'がに含まれていないので#exit_with_initialization_warning!が実行されるのかと思ったけど#require_command!が実行されている。分からない。
  • #require_command!ではrails/commands/applicationをrequireしている。

 

require "rails/commands/#{command}"を追いかける。

#railties/lib/rails/commands/application.rb

require 'rails/generators'
require 'rails/generators/rails/app/app_generator'

module Rails
  module Generators
    class AppGenerator # :nodoc:
      # We want to exit on failure to be kind to other libraries
      # This is only when accessing via CLI
      def self.exit_on_failure?
        true
      end
    end
  end
end

# ARGV => ['new', 'my_app']
args = Rails::Generators::ARGVScrubber.new(ARGV).prepare!
# args => ['my_app']
Rails::Generators::AppGenerator.start args
  • ARGVについている各オプションを元にargsに整形する。今回はオプションをつけていないのであまり関係ない。
  • argsを渡してRailsの雛形ファイル群を生成すると思われる(コードが追えなくなったので確証はない)

 

args = Rails::Generators::ARGVScrubber.new(ARGV).prepare!を追いかける。

#railties/lib/rails/generater/rails/app/app_generator.rb

require 'rails/generators/app_base'

module Rails
  # ...

  module Generators
    # We need to store the RAILS_DEV_PATH in a constant, otherwise the path
    # can change in Ruby 1.8.7 when we FileUtils.cd.
    RAILS_DEV_PATH = File.expand_path("../../../../../..", File.dirname(__FILE__))
    RESERVED_NAMES = %w[application destroy plugin runner test]

    # ...

    class ARGVScrubber # :nodoc:
      def initialize(argv = ARGV)
        @argv = argv
        # @argv => ['new', 'my_app']
      end

      def prepare!
        # @argv => ['new', 'my_app']
        handle_version_request!(@argv.first)
        handle_invalid_command!(@argv.first, @argv) do
          handle_rails_rc!(@argv.drop(1))
        end
      end

      def self.default_rc_file
        File.expand_path('~/.railsrc')
      end

      private

        def handle_version_request!(argument)
          # argument => 'new'
          if ['--version', '-v'].include?(argument)
            require 'rails/version'
            puts "Rails #{Rails::VERSION::STRING}"
            exit(0)
          end
        end

        def handle_invalid_command!(argument, argv)
          # argument => 'new'
          # argv => ['new', 'my_app']
          if argument == "new"
            yield
          else
            ['--help'] + argv.drop(1)
          end
        end

        def handle_rails_rc!(argv)
          # argv => ['my_app']
          if argv.find { |arg| arg == '--no-rc' }
            argv.reject { |arg| arg == '--no-rc' }
          else
            railsrc(argv) { |rc_argv, rc| insert_railsrc_into_argv!(rc_argv, rc) }
          end
        end

        def railsrc(argv)
          #argv => ['my_app']
          if (customrc = argv.index{ |x| x.include?("--rc=") })
            fname = File.expand_path(argv[customrc].gsub(/--rc=/, ""))
            yield(argv.take(customrc) + argv.drop(customrc + 1), fname)
          else
            yield argv, self.class.default_rc_file
          end
        end

        # ...

        def insert_railsrc_into_argv!(argv, railsrc)
          # argv => ['my_app']
          # railsrc => '~/.railsrcの絶対パス'
          return argv unless File.exist?(railsrc)
          extra_args = read_rc_file railsrc
          argv.take(1) + extra_args + argv.drop(1)
        end
    end
  end
end
  • #handle_version_request!ではargument'--version','-v'が含まれていないので関係ない。
  • #handle_invalid_command!ではargument'new'なのでブロックにそのままargument, argvを渡す。
  • #handle_rails_rc!ではargv'--no--rc'が含まれていないので#railsrcを実行する。
  • #railsrcではargvの各要素に'--rc='が含まれていないのでブロックにargv, self.class.default_rc_fileを渡す。
  • #self.default_rc_fileでは~/.railsrc絶対パスを取得する。なんでこのメソッドはprivateメソッドじゃなくて特異メソッドなんだろう。
  • #insert_railsrc_into_argv!では~/.railsrcが存在しないのでargvを返す。

 

Rails::Generators::AppGenerator#startを探す旅が始まる。
Rails::Generators::AppGenerator.start argsを追いかける。

#railties/lib/rails/generators/rails/app/app_generator.rb

require 'rails/generators/app_base'

module Rails
  # ...

  module Generators
    # ...

    class AppGenerator < AppBase # :nodoc:
      # ...

    end
  end
end
  • Rails::Generators::AppGeneratorでは#startは定義されていなかった。
  • AppBaseを継承していた。

 

rails/generators/app_baseをチェック。

#railties/lib/rails/generators/app_base.rb

require 'digest/md5'
require 'active_support/core_ext/string/strip'
require 'rails/version' unless defined?(Rails::VERSION)
require 'open-uri'
require 'uri'
require 'rails/generators'
require 'active_support/core_ext/array/extract_options'

module Rails
  module Generators
    class AppBase < Base # :nodoc:
      # ...

    end
  end
end
  • Rails::Generators::AppBaseでは#startは定義されていなかった。
  • Baseを継承していた。どこでrequireしてるのか分からなかった。

 

rails/generators/baseをチェック。

#railties/lib/rails/generators/base.rb

begin
  require 'thor/group'
rescue LoadError
  puts "Thor is not available.\nIf you ran this command from a git checkout " \
       "of Rails, please make sure thor is installed,\nand run this command " \
       "as `ruby #{$0} #{(ARGV | ['--dev']).join(" ")}`"
  exit
end

module Rails
  module Generators
    # ...

    class Base < Thor::Group
      # ...

    end
  end
end
  • Rails::Generators::Baseでは#startは定義されていなかった。
  • Thor::Groupを継承していた。外部のgemに行ってしまった。

 

thor/groupをチェック。

#thor/lib/thor/group.rb

require "thor/base"

class Thor::Group
  class << self
    # ...

    include Thor::Base
end
  • Thor::Groupでは#startは定義されていなかった。
  • Thor::Baseをインクルードしていた。

 

thor/baseをチェック。

#thor/lib/thor/base.rb

require "thor/command"
require "thor/core_ext/hash_with_indifferent_access"
require "thor/core_ext/ordered_hash"
require "thor/error"
require "thor/invocation"
require "thor/parser"
require "thor/shell"
require "thor/line_editor"
require "thor/util"

class Thor
  autoload :Actions,    "thor/actions"
  autoload :RakeCompat, "thor/rake_compat"
  autoload :Group,      "thor/group"

 # ...

  module Base
    # ...

    class << self
      def included(base) #:nodoc:
        base.extend ClassMethods
        base.send :include, Invocation
        base.send :include, Shell
      end

      # ...

    end

    module ClassMethods
      # ...

      def start(given_args = ARGV, config = {})
        # given_args => ['my_app']
        # config => {}
        config[:shell] ||= Thor::Base.shell.new
        # config { shell: Thor::Shell::Colorオブジェクト }
        dispatch(nil, given_args.dup, nil, config)
      rescue Thor::Error => e
        config[:debug] || ENV["THOR_DEBUG"] == "1" ? (raise e) : config[:shell].error(e.message)
        exit(1) if exit_on_failure?
      rescue Errno::EPIPE
        # This happens if a thor command is piped to something like `head`,
        # which closes the pipe when it's done reading. This will also
        # mean that if the pipe is closed, further unnecessary
        # computation will not occur.
        exit(0)
      end

      # ...
    end
  end
  • Thor::Baseがインクルードされたときフックメソッドのincluded()が実行される。included()Thor::Base::ClassMethodsをextendしてThor::Base::ClassMethodsのメソッドがThor::Baseのクラスメソッドとして使えるようにしている。
  • Thor::Base::ClassMethod#startでは設定や開発環境に合わせてshellでの出力を指定してRailsの雛形ファイル群を生成している。

 

config[:shell] ||= Thor::Base.shell.newを追いかける。

#thor/lib/thor/shell.rb

require "rbconfig"

class Thor
  module Base
    class << self
      attr_writer :shell

      # Returns the shell used in all Thor classes. If you are in a Unix platform
      # it will use a colored log, otherwise it will use a basic one without color.
      #
      def shell
        @shell ||= if ENV["THOR_SHELL"] && ENV["THOR_SHELL"].size > 0
          Thor::Shell.const_get(ENV["THOR_SHELL"])
        # macの場合 RbConfig::CONFIG["host_os"] => 'darwin'
        # windows系の場合 RbConfig::CONFIG["host_os"] => 'mswin' or 'mingw'
        elsif RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ && !ENV["ANSICON"]
          Thor::Shell::Basic
        else
          Thor::Shell::Color
        end
      end
    end
  end

  # ...

end
  • 環境変数を設定してなくてPCはmacだとしたとき、Thor::Base#shellThor::Shell::Colorを返す。
  • rails new my_appを実行したときに出力に色がついていたのはこの辺の設定だったのか。

 

dispatch(nil, given_args.dup, nil, config)を追いかける。

#thor/lib/thor/group.rb

require "thor/base"

class Thor::Group # rubocop:disable ClassLength
  class << self
    # ...

  protected

    # The method responsible for dispatching given the args.
    def dispatch(command, given_args, given_opts, config) #:nodoc:
      # command => nil
      # given_args => ['my_app']
      # given_opts => nil
      # config => { shell: Thor::Shell::Colorのインスタンス }

      # Thor::HELP_MAPPINGS => ['-h', '-?', '--help', '-D']
      if Thor::HELP_MAPPINGS.include?(given_args.first)
        help(config[:shell])
        return
      end

      args, opts = Thor::Options.split(given_args)
      # args => ['my_app']
      # opts => []
      opts = given_opts || opts
      # opts => []

      instance = new(args, opts, config)
      yield instance if block_given?

      if command
        instance.invoke_command(all_commands[command])
      else
        instance.invoke_all
      end
    end
  • instance = new(args, opts, config)で突如出現する#newが何をnewしているのか分からなかったけどデバッグするとRails::Generators::AppGeneratorだと判明した。
  • commandnilなので#invoke_allを実行している。

 

instance.invoke_allを追いかける。

#thor/lib/thor/invocation.rb

class Thor
  module Invocation

    # Make initializer aware of invocations and the initialization args.
    def initialize(args = [], options = {}, config = {}, &block) #:nodoc:
      @_invocations = config[:invocations] || Hash.new { |h, k| h[k] = [] }
      @_initializer = [args, options, config]
      super
    end

    # ...

    # Invoke the given command if the given args.
    def invoke_command(command, *args) #:nodoc:
      # self.class => Rails::Generators::AppGenerator
      current = @_invocations[self.class]

      unless current.include?(command.name)
        current << command.name
        command.run(self, *args)
      end
    end

    # Invoke all commands for the current instance.
    def invoke_all #:nodoc:
      self.class.all_commands.map { |_, command| invoke_command(command) }
    end

    # ...
  end
end
  • #invoke_allでデフォルトの雛形ファイル群を全部生成している。
  • #invoke_all内のself.class.all_commandsでは以下のようなキーがコマンド名で値がThor::Commandオブジェクトのハッシュを生成している。
{
  "set_default_accessors!"=>
    #<struct Thor::Command 
      name = "set_default_accessors!",
      description = nil, 
      long_description = nil, 
      usage = nil, 
      options = {}
    >, 
  "create_root"=>
    #<struct Thor::Command 
      name = "create_root", 
      description = nil, 
      long_description = nil, 
      usage = nil, 
      options = {}
    >,
  "create_root_files"=>
    #<struct Thor::Command 
      name = "create_root_files", 
      description = nil, 
      long_description = nil, 
      usage = nil, 
      options = {}
    >, ... 
}
  • #invoke_allで処理を個々に切り分け、#invoke_commandThor::Commandオブジェクトを渡してファイルを生成している。
  • #invoke_commandでは実行するコマンドが重複しないようにcurrentで管理している。

 

command.run(self, *args)を追いかける。

#thor/lib/thor/command.rb

class Thor
  class Command < Struct.new(:name, :description, :long_description, :usage, :options)

    def initialize(name, description, long_description, usage, options = nil)
      super(name.to_s, description, long_description, usage, options || {})
    end

    # ...

    # By default, a command invokes a method in the thor class. You can change this
    # implementation to create custom commands.
    def run(instance, args = [])
      # instance => Rails::Generators::AppGeneratorオブジェクト
      arity = nil

      if private_method?(instance)
        instance.class.handle_no_command_error(name)
      elsif public_method?(instance)
        arity = instance.method(name).arity
        instance.__send__(name, *args)
      elsif local_method?(instance, :method_missing)
        instance.__send__(:method_missing, name.to_sym, *args)
      else
        instance.class.handle_no_command_error(name)
      end
    rescue ArgumentError => e
      handle_argument_error?(instance, e, caller) ? instance.class.handle_argument_error(self, e, args, arity) : (raise e)
    rescue NoMethodError => e
      handle_no_method_error?(instance, e, caller) ? instance.class.handle_no_command_error(name) : (fail e)
    end

    # ...

  protected
    # ...

    # Given a target, checks if this class name is a public method.
    def public_method?(instance) #:nodoc:
      !(instance.public_methods & [name.to_s, name.to_sym]).empty?
    end

    def private_method?(instance)
      !(instance.private_methods & [name.to_s, name.to_sym]).empty?
    end

    def local_method?(instance, name)
      methods = instance.public_methods(false) + instance.private_methods(false) + instance.protected_methods(false)
      !(methods & [name.to_s, name.to_sym]).empty?
    end
  • #runではRails::Generators::AppGeneratorにコマンド名と同一のpublicメソッドが存在したとき、Rails::Generators::AppGenerator#コマンド名を実行している。
  • Rails::Generators::AppGeneratorにコマンド名と同一のprotectedメソッドが存在したときはNoMethodErrorを返し、同一のprivateメソッドが存在したときと同一のメソッドが存在しなかったときはUndefinedCommandErrorを返す仕様になっている。一体何の差なんだろうか。

 

instance.__send__(name, *args)を追いかける。

#railties/lib/rails/generators/rails/app/app_generators.rb

require 'rails/generators/app_base'

module Rails
  # ...

  module Generators    
    # ...

    class AppGenerator < AppBase # :nodoc:

      public_task :set_default_accessors!
      public_task :create_root

      def create_root_files
        build(:readme)
        build(:rakefile)
        build(:configru)
        build(:gitignore) unless options[:skip_git]
        build(:gemfile)   unless options[:skip_gemfile]
      end

      def create_app_files
        build(:app)
      end

      def create_bin_files
        build(:bin)
      end

      # ... コマンド名と同じメソッドが続く
  • やっとRailsに戻ってきた。
  • それぞれのメソッドで#buildを実行している。
  • 普通のメソッドではなくpublic_taskで定義されているものもあるけど疲れたので放置。

 

build()を追いかける。

#railties/lib/rails/generators/app_base.rb

require 'digest/md5'
require 'active_support/core_ext/string/strip'
require 'rails/version' unless defined?(Rails::VERSION)
require 'open-uri'
require 'uri'
require 'rails/generators'
require 'active_support/core_ext/array/extract_options'

module Rails
  module Generators
    class AppBase < Base # :nodoc:
      # ...

    protected
      # ...

      def builder
        @builder ||= begin
          builder_class = get_builder_class
          # builder_class => Rails::AppBuilder
          builder_class.send(:include, ActionMethods)
          # self => Rails::Generators::AppGeneratorオブジェクト
          builder_class.new(self)
        end
      end

      def build(meth, *args)
        builder.send(meth, *args) if builder.respond_to?(meth)
      end
  • #builderではRails::ActionMethodsをインクルードしてRails::ActionMethods#newを用いてRails::Generators::AppGeneratorsオブジェクトを引数にRails::AppBuilderを生成している(はず)。
  • #buildではmethと同一の#builderで生成したRails::AppBuilderオブジェクトのインスタンスメソッドを実行している。

 

builder.send(meth, *args)を追いかける。

#railties/lib/rails/generators/rails/app/app_generators.rb

require 'rails/generators/app_base'

module Rails
  # ...

  class AppBuilder
    def rakefile
      template "Rakefile"
    end

    def readme
      copy_file "README.rdoc", "README.rdoc"
    end

    def gemfile
      template "Gemfile"
    end

    def configru
      template "config.ru"
    end

    def gitignore
      template "gitignore", ".gitignore"
    end

    def app
      directory 'app'

      keep_file  'app/assets/images'
      keep_file  'app/mailers'
      keep_file  'app/models'

      keep_file  'app/controllers/concerns'
      keep_file  'app/models/concerns'
    end

    def bin
      directory "bin" do |content|
        "#{shebang}\n" + content
      end
      chmod "bin", 0755 & ~File.umask, verbose: false
    end

    # ... methと同じメソッドが続く
  end
  
  # ...

end
  • #template#directory, #keep_fileThor::Actionのメソッド。
  • 各行で何が生成されてるか分かるようになってきたのでそろそろ終わろう。

 

おわりに

  • RailsRubyそのものについての様々な知見が得られた。
  • Rails開発者に足を向けて寝られなくなった。

 

参考にさせていただきました

`rails s`読んだ - AnyType

【大学編入】編入勉強を始める前にやったこと

僕が編入勉強を始める前にやったことをまとめてみました。
勉強を始めるまでにやったことは大きく分けて4つあります。

step1 どんな大学生活を送りたいかを考える
step2 大学を選ぶ
step3 合格するための情報を集める
step4 勉強計画を立てる

 

step1 どんな大学生活を送りたいかを考える

まずはどんな大学生活を送りたいか考えます。

研究漬けの生活を送りたい、パリピになりたい、など色々あると思います。
この時点で「〇〇大学の△△研究室で研究したい」という具体的な希望がある人は step3.合格するための情報を集める に進みましょう。

 

僕の場合は次のようになりました。

  • webと機械学習の知識を生かせる研究をしたい
  • 総合大学に行きたい
  • 技術系のアルバイトがしたい
  • 大学外での活動も充実させたい

 

step2 大学を選ぶ

step1で考えた内容を元に大学を決めます。

 

僕の場合だと

  • webと機械学習の知識を生かせる研究をしたい
    →webを中心とした自然言語処理の研究室がある大学

  • 総合大学に行きたい
    →総合大学

  • 技術系のアルバイトがしたい
    →そもそも技術系のアルバイトをするためにはIT企業が必要
    →IT企業が多いのは東京
    →東京にアクセスが容易な大学

  • 大学外での活動も充実させたい
    →勉強以外の時間もしっかり取れる
    高専生の編入実績が多く、単位互換がしやすい大学

これらの条件から大学を探した結果、 筑波大学がヒットしました。

 

step3 合格するための情報を集める

step2で決めた大学に合格するために必要な情報を集めます。

必要な情報とは

  • 編入試験の範囲
  • 先人の勉強記録
  • よく使われている参考書

などです。

方法としては

  • 各大学の募集要項をチェックする
  • 編入ブログを読む
  • 高専で行われている編入セミナーに参加する
  • 編入説明会に参加する
  • Studyplusで検索する

など色々あると思います。

 

step4 勉強計画を立てる

必要な情報が集まったら勉強計画を立てます。
ここで重要になってくるのが「この計画を遂行すれば必ず合格する」と思い込めるような計画を立てることです。 必ず合格すると思い込める計画を立てられると本当に勉強が嫌になったときに踏ん張ることができます。

そして合格すると思い込むためにはstep3で情報を大量に集めて自分の中で合格ラインを作ることが大切です。
5人の編入ブログから作った合格ラインと50人の編入ブログから作った合格ラインだと信頼性が全然違うと思いませんか?

ただ注意してほしいのが「勉強計画は何度も立て直すもの」ということです。
勉強を始める前に作った勉強計画は現実的でなかったり効率があまり良くないことが多いです(体験談)。
無理な計画や無駄の多い計画に気づいたら早めに修正してロスを減らしましょう。

個人的には「1日で○時間勉強する」といった目標よりも「1日でこの参考書の問題を○問解く」といった目標の方がダラけずに勉強を続けやすかったです。

 

 

僕はこの方法で勉強計画まで落とし込みましたが方法は色々あると思います。
他の人の記事も参考にしながら自分が納得する方法を見つけてください。

これから編入を考えている方頑張ってください。

 
編入体験記をまとめました。
euglena1215.hatenablog.jp

MacBook ProでCUDAのGPUモードが使えたり使えなかったりする話

MacBook ProでTheanoのGPUモードを使うために設定を済ませてimportするとエラーが発生。
GPUモードが使えない...??

>>> import theano
WARNING (theano.sandbox.cuda): CUDA is installed, but device gpu is not available
  (error: Unable to get the number of gpus available: CUDA driver version is insufficient for CUDA runtime version)

 

自分のmacのグラフィックスを確認すると
NVIDIA製ではないので使えないのは当たり前でした。

  チップセットのモデル: Intel Iris Graphics 6100  
  種類:  GPU  
  バス:  内蔵  
  VRAM(ダイナミック、最大):  1536 MB  
  製造元:  Intel (0x8086)

 

でも調べているとiMacではCUDAのGPUモードが使えている人はいる様子。
同じApple製品で違うメーカーのGPUが載ってるってこと?
なんでだろう。

ket-30.hatenablog.com

 

さらに調べているとこんな記事が

MacBook Pro現行モデルの技術仕様を見ると最上位機種のグラフィックスチップはIntel Iris Pro GraphicsとAMD Radeon R9 M370Xとなっており、NVIDIA GeForceは搭載されていません。

ですが、MacBook Pro (15-inch, Mid 2012) - 技術仕様を見ると、私が持っているMacBook Proは15インチ2.6GHzモデルなのでNVIDIA GeForce GT 650M、1GB GDDR5メモリが搭載されています。

GeForce GT 650M | NVIDIAには「プログラミング環境」の行に「CUDA」とあるのでCUDAが使えるようです。

引用: GeForce搭載の旧モデルMacBook ProでCUDAをセットアップする手順のメモ · hnakamur's blog at github

昔のMacBook ProではGeForceを選択できたみたい。

というわけでGeForceが載っているものと載っていないものを2011~2015年で調べてみました。

GeForceが載っている
  • Retina, 15-inch, Mid 2014
  • Retina, 15-inch, Early 2013
  • 15-inch, Mid 2012
GeForceが載ってない
  • Retina, 15-inch, Mid 2015
  • Retina, 13-inch, Early 2015
  • Retina, 13-inch, Mid 2014
  • Retina, 13-inch, Late 2013
  • Retina, 13-inch, Early 2013
  • Retina, 13-inch, Late 2012
  • 13-inch, Mid 2012
  • 17-inch, Late 2011
  • 13-inch, Late 2011
  • 15-inch, Late 2011
  • 17-inch, Early 2011
  • 15-inch, Early 2011
  • 13-inch, Early 2011

まとめ

MacBook ProでCUDAのGPUモードを使うなら2012~2014年モデルの15インチを買うべし。

 

その他のApple製品の技術仕様はこちらからどうぞ
アップル – サポート - 技術仕様

graphvizを使って木構造を可視化する関数を作ってみた

卒業研究で木構造を可視化させる必要があったので関数を作ってみました。

木構造・ノードクラスの属性は次のようになります。

Tree クラス
属性名 型,クラス 説明
root Node Treeの根ノード
Node クラス
属性名 型,クラス 説明
num int ノード番号
child Node 子ノード
brother Node 兄弟ノード(木構造で考えると右側のノード)
parent Node 親ノード

 

ソースコード

 

現在研究している自己生成ニューラル木に菖蒲の花のデータセットを学習させたものを可視化させるとこのようになりました。 f:id:euglena1215:20160930153903p:plain

関数を少し修正して花の種類で色分けするとこのようになりました。 f:id:euglena1215:20160930155224p:plain

 

木構造が可視化できると色々捗りますよね。

参考にさせていただきました

PythonからGraphvizを使う - Leaaaaaaaaaaaaarning
画像処理についてあれこれ: Graphvizでノードの塗りつぶし色を指定する

Railsでセキュリティのことを考えずに作ったwebアプリは脆弱性があるのか調べてみた(脆弱性検証編)

この続きです。 euglena1215.hatenablog.jp

検証方法が全く網羅的ではないということを踏まえた上でお読みください。

検証

XSS(クロスサイトスクリプティング)

方法

投稿フォームに以下のリンクに載っているXSSの例を片っ端から入力してみた。
XSS Filter Evasion Cheat Sheet - OWASP

結果

脆弱性は見つからなかった。

理由

rails3からデフォルトで文字列を出力するときにはエスケープされるようになっているため基本的にはXSSは通らない。

なので文字列をエスケープさせたくない場合には
<%= 文字列.html_safe %>
<%= raw 文字列 %>
<%== 文字列 %>
のどれかを使う。

qiita.com

この記事によると <%== を使うのがベストらしい。

SQLインジェクション

方法

検索フォームに ');-- と入力する

結果

SELECT "users"."id" FROM "users" WHERE (username LIKE '%');--%’)
というクエリが生成され、userの全件取得ができてしまう。ということは…

 


 

方法

検索フォームに’ AND password LIKE ‘password’);— と入力する

結果

SELECT "users"."id" FROM "users" WHERE (username LIKE '%’ AND password LIKE ‘password’);—%')
というクエリが発行されてpasswordが’password’のユーザが取得できる。
という風に利用者が任意のクエリを発行することができる。これは危ない。
※今回使用したDeviseは暗号化したパスワードをDBに保存しているのですぐにパスワードがバレるというわけではありません

普通に利用しているとユーザがアクセスできないデータにアクセスされる可能性がある。

対策

where("username LIKE '%"+username+"%’”)
where("username LIKE ?","%#{username}%") に変更する。

以下のように修正する。

# before
  # app/models/post.rb
  scope :by_body_like, ->(body){
    where("body LIKE '%"+body+"%'")
  }
  # app/models/user.rb
  scope :by_username_like, ->(username){
    where("username LIKE '%"+username+"%'")
  }
# after
  # app/models/post.rb
  scope :by_body_like, ->(body){
    where("body LIKE :body ESCAPE '\\'", { body: "%#{sanitize_sql_like(body)}%"})
  }
  # app/models/user.rb
  scope :by_username_like, ->(username){
    where("username LIKE :username ESCAPE '\\'", { username: "%#{sanitize_sql_like(username)}%"})
  }

検索でLIKE演算子ワイルドカードである_や%が使われてもいいようにエスケープする必要がある。
Rails 4.2.1以降にはsanitize_sql_likeメソッドが実装されているのでそれを使う。
sanitize_sql_like (ActiveRecord::Sanitization::ClassMethods) - APIdock

?を使うとSQLインジェクションを防ぐことができるということは分かったものの、なぜこれでSQLインジェクションが防げるのかよく分からなかった。
なので今度Active recordを読み解いていこうと思う。
Active record whereメソッド :
rails/query_methods.rb at 0399b71dab8b270b4e40b2aff99194a8b8f2596c · rails/rails · GitHub

これはActive recordのwhereメソッドの仕様ではなくSQLプレースホルダという機能だった。
プレースホルダとはパラメータを後で記述できるようにすることによって、SQLの構文を決定した後にバインドできるのでパラメータがリテラルの外にはみ出す現象が起こらなくなりSQLインジェクションが起こらなくなる機能だ。

 
 

普段SQL文を書いていないことが露呈してしまいました...基礎技術を勉強し直そうと思います。
ご指摘ありがとうこざいます!
 

?を使うSQL文は色々なところで見たことはありましたがプレースホルダという名前があったのは知りませんでした。
ご指摘のおかげでgoogle先生に聞くことができました。ありがとうございます!


方法

検索フォームに ‘); DROP “posts”;— '); DROP TABLE “posts”;-- と入力する
SQLインジェクションが通る状態で複文はどうなるのか試してみた。

結果

SELECT "users"."id" FROM "users" WHERE (username LIKE '%‘); DROP “posts”;--%')
SELECT "users"."id" FROM "users" WHERE (username LIKE '%'); DROP TABLE “posts”;--%') というクエリが発行されているもののpostsテーブルは削除されていなかった。
なので複文はサポートされてないみたい。(今回はSQLite3での実験だったので他のDBでどうなるかは試していません)

まとめ

  • やはり自分の書いたコードに穴があった。
  • XSSSQLインジェクションチートシートの存在を知ったことが一番の収穫だった。
  • マサカリを投げて間違いを指摘してくれる人がいる。アウトプットの重要性を改めて実感した。

 

参考にさせていただきました

SQL Injection Cheat Sheet
Railsで検索をSQLで簡単に実装する方法 - Qiita
Railsのセキュリティ対策で調べた事 - Qiita
XSS Filter Evasion Cheat Sheet - OWASP
html_safe、raw、「<%==」の比較 - Qiita
https://www.ipa.go.jp/files/000017320.pdf
rails - クエリの基本 - そういうことだったんですね