20190529のRubyに関する記事は22件です。

Rspecで突然謎のエラーが

circleCIで通常通り動いていたテストコードが突然動かなくなりましたが解決したのでその方法を書いていきます。

開発環境

Ruby 2.6.2
Rails 5.2.3
Docker 18.09.2
docker-compose 1.23.2

今回発生したエラー

Selenium::WebDriver::Error::UnknownError:
    unknown error: session deleted because of page crash
    from unknown error: cannot determine loading status
    from tab crashed

原因

エラーを調べたところ日本語での記事が少なく詳しくはわからなかったのですが、メモリ不足によってクラッシュしてしまうとのこと。
ブラウザのサイズを小さくすれば解決するとの記事を見て試してみたのですが、僕の場合小さくしても解決しませんでした。

解決方法

rails_helperのchromeOptionsのところに--no-sandbox--disable-dev-shm-usageというargumentsを追加してください。--disable-dev-shm-usageと書くことでChromeは/tmpディレクトリを代わりに使用するようになるみたいです。メモリの代わりにディスクが使用されるので、実行が遅くなる場合があるみたいですがそんなに変わってない気がします。

spec/rails_helper.rb
Capybara.register_driver :selenium_remote do |app|
  driver = Capybara::Selenium::Driver.new(
      app,
      browser: :remote,
        desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome(
            chromeOptions: {
                args: [
                    'window-size=500,500', #念の為サイズも小さく
                    'headless',
                    '--no-sandbox', # crush回避
                    '--disable-dev-shm-usage' # crush回避
                ]
            }
        ),
        url: 'http://chrome:4444/wd/hub',
    )
end

これで解決すると思います。

もし間違いがあれば訂正お願いいたします。

参考サイト

https://qiita.com/jb-vasseur/items/d3aa33e08ebba3b9231e
https://stackoverflow.com/questions/53902507/unknown-error-session-deleted-because-of-page-crash-from-unknown-error-cannot

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【1分で読める】プログラミングスクールに通って1ヶ月で学んだこと(できるようになったこと)、感じたこと

WEBエンジニアに転向するため2019年5月から某プログラミングスクールに通っています。執筆時点で約1ヶ月経過したので、学んだことやら、感じたことやらを書き記します。スクール自体はあと数ヶ月続きます。

プログラミングスクールに通うか迷っている人の参考になればと思います。(進捗はあくまでも自分の場合です!周りを見ると、進捗は本当にバラバラ)

なんでプログラミングスクールに入ったのかーとか諸々は卒業後にまたまとめて書こうかなと!

スペックとか

  • 言語:Ruby
  • 年齢:29
  • 性別:男
  • 5月からプログラミングスクールに通ってるけど、4月15日から事前に学習をスタート
  • プログラミング歴はなし!学生時代に授業でやったくらい
  • 勉強時間は7〜10時間/日くらい。頭が疲れたら、基本的にやらない感じ

学んだこと(できるようになったこと)

Ruby(Rails)を使ってtwitter,facebook,instagramのようなアプリは作れるようになった!!!(コードのコピペじゃない!)

投稿機能はもちろん、投稿のお気に入り機能、ユーザのフォローフォロワー機能を実装したり、LINEみたいなメッセージのやりとり機能を実装したりもできるようになった。Ajaxを使ったりも。

さらにherokuを使えば簡単に全世界に公開もできる。すごすぎるぞ、自分!!

デザインとか、サーバのこととか、セキュリティとか考えるとまだまだかもしれないけど、1ヶ月半くらいで、railsアプリ開発の基本的なことはできるようになったのかなと勝手に思ってます。

あとは、リファクタリングとかコーディング規約を守るとか頑張らないとなーって感じ。
(本当はもっとあるんだろうな…)

感じたこと

  • 想像していたものがブラウザ上で形になるのは楽しい!嬉しい!
  • 上っ面のところしか理解できていない感がとてもある
  • 一人で黙々とやっていたらこんなにハイペース?で勉強を進められないだろうなと実感

→同期、メンターの存在のおかげで頑張ろって思えるし、分からないことも教えあえる。また、一人じゃ気付けなさそうなことも会話から気づきを得ることができる。感謝感謝。
一人ならダラダラと勉強しちゃいそう。。

これからのこと

  • サーバ(AWS)のことや、フロント(Vue.js)のことを学んでいくぞい!
  • RubyやRailsも知れば知るほど、新たな疑問が生まれるのでもっともっと理解を深めていくぞい!
  • 就活も始めるぞい!

乞うご期待!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

様々なプログラミング言語でクラス (あるいはクラスもどき) を書く

概要

  • 様々なプログラミング言語でクラス (あるいはクラスもどき) を書く

Java

MyCounter.java

public class MyCounter {

  public static MyCounter globalCounter = null;

  private String name = "No Name";
  private int count;

  public MyCounter(int initValue) {
    count = initValue;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public int addCount(int number) {
    count += number;
    return count;
  }

  public void print() {
    System.out.println(format());
  }

  private String format() {
    return name + ": " + count;
  }

  public static void createGlobalCounter(String name, int initValue) {
    globalCounter = new MyCounter(initValue);
    globalCounter.setName(name);
  }
}

MyMain.java

public class MyMain {

  public static void main(String[] args) {

    MyCounter counter = new MyCounter(0);
    counter.print();

    counter.setName("John Doe");
    String currentName = counter.getName();
    System.out.println("Current Name=" + currentName);

    int currentValue = counter.addCount(10);
    System.out.println("Current Value=" + currentValue);

    counter.print();

    MyCounter.createGlobalCounter("CafeBabe! Write once, run anywhere", 256);
    MyCounter.globalCounter.print();
  }
}

実行結果

No Name: 0
Current Name=John Doe
Current Value=10
John Doe: 10
CafeBabe! Write once, run anywhere: 256

JavaScript

my_counter.js

class MyCounter {

  constructor(initValue) {
    this._name = 'No Name';
    this.count = initValue;
  }

  get name() {
    return this._name;
  }

  set name(name) {
    this._name = name;
  }

  addCount(number) {
    this.count += number;
    return this.count;
  }

  print() {
    console.log(this._format());
  }

  _format() {
    return this._name + ': ' + this.count;
  }

  static createGlobalCounter(name, init_value) {
    MyCounter.global_counter = new MyCounter(init_value);
    MyCounter.global_counter.name = name;
  }
}

module.exports = MyCounter;

my_main.js

const MyCounter = require('./my_counter');

counter = new MyCounter(0);
counter.print();

counter.name = 'John Doe';
const currentName = counter.name;
console.log('Current Name=' + currentName);

const currentValue = counter.addCount(10);
console.log('Current Value=' + currentValue);

counter.print();

MyCounter.createGlobalCounter('JavaScript was born from Netscape Navigator 2.0', 256);
MyCounter.global_counter.print();

実行結果

$ node my_main.js
No Name: 0
Current Name=John Doe
Current Value=10
John Doe: 10
JavaScript was born from Netscape Navigator 2.0: 256

Python

my_counter.py

class MyCounter:

  global_counter = None

  def __init__(self, init_value):
    self.__name = "No Name"
    self.__count = init_value

  @property
  def name(self):
    return self.__name

  @name.setter
  def name(self, name):
    self.__name = name

  def add_count(self, number):
    self.__count += number
    return self.__count

  def print(self):
    print(self.__format())

  def __format(self):
    return "{0}: {1}".format(self.__name, self.__count)

  @classmethod
  def create_global_counter(cls, name, init_value):
    cls.global_counter = MyCounter(init_value)
    cls.global_counter.name = name

my_main.py

from my_counter import MyCounter

counter = MyCounter(0)
counter.print()

counter.name = "John Doe"
current_name = counter.name
print("Current Name={}".format(current_name))

current_value = counter.add_count(10)
print("Current Value={}".format(current_value))

counter.print()

MyCounter.create_global_counter("Batteries included. Beautiful is better than ugly.", 256)
MyCounter.global_counter.print()

実行結果

$ python my_main.py
No Name: 0
Current Name=John Doe
Current Value=10
John Doe: 10
Batteries included. Beautiful is better than ugly.: 256

Ruby

my_counter.rb

class MyCounter
  attr_accessor :name

  @@global_counter = nil

  def initialize(init_value)
    @name = 'No Name'
    @count = init_value
  end

  def add_count(number)
    @count += number
  end

  def print
    puts format
  end

  private

  def format
    "#{@name}: #{@count}"
  end

  def self.create_global_counter(name, init_value)
    @@global_counter = self.new(init_value)
    @@global_counter.name = name
  end

  def self.global_counter
    @@global_counter
  end
end

my_main.rb

require './my_counter'

counter = MyCounter.new(0)
counter.print

counter.name = 'John Doe'
current_name = counter.name
puts "Current Name=#{current_name}"

current_value = counter.add_count(10)
puts "Current Value=#{current_value}"

counter.print

MyCounter.create_global_counter('Everything is an object in Ruby', 256)
MyCounter.global_counter.print
$ ruby my_main.rb
No Name: 0
Current Name=John Doe
Current Value=10
John Doe: 10
Everything is an object in Ruby: 256
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby/Ruby on rails/MysqlをDockerで環境構築

DockerでRuby/Ruby on rails/mysqlの環境構築からデプロイまでできたのでメモしておきます。

(1)Dockerfile作成

Dockerfile

FROM ruby:2.5.3


RUN apt-get update -qq && \
    apt-get install -y build-essential \ 
                       libpq-dev \        
                       nodejs           

RUN mkdir /app_name 

ENV APP_ROOT /app_name 
WORKDIR $APP_ROOT

ADD ./Gemfile $APP_ROOT/Gemfile
ADD ./Gemfile.lock $APP_ROOT/Gemfile.lock


RUN bundle install
ADD . $APP_ROOT

(2)docker-compose.yml作成

docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: root
    ports:
      - "3306:3306"

  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/app_name
    ports:
      - "3000:3000"
    links:
      - db

(3)Gemfile作成

Gemfile
source 'https://rubygems.org'
gem 'rails', '5.2.2'

Gemfile.lockも作成
$ touch Gemfile.lock

(4)新しいアプリを作成

$ docker-compose run web rails new アプリ名 . --force --database=mysql --skip-bundle

databese.ymlを編集

database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password 
  host: db 

Gemfileをbundle install

$ docker-compose run web bundle install

(5)コンテナをビルドと起動

$ docker-compose build # コンテナをビルド

$ docker-compose up # コンテナの一斉起動

※bundle installの後にbuildする

(6)DB作成

$ docker-compose run web rails db:create

DBを作らないとエラーが出る

DBを作成後localhost:3000で初期画面が表示される!

()Herokuでデプロイ

herokuの登録,heroku cliはインストール済みとする

Herokuにアプリ作成

$ heroku create

データベースをMySQLに変更

$ heroku addons:add cleardb

ClearDBアドオンとは,ClearDBというデータベースサービスが提供している,MySQLを使うためのもの。

$ heroku config | grep CLEARDB_DATABASE_URL
mysql:// 〜 reconnect=trueがデータベース情報

mysqlの部分をmysql2に変更

環境変数を変更

$ heroku config:set DATABASE_URL=mysql2:// 〜 reconnect=true     //先ほどのデータベース情報全部
$ git add .
$ git commit -m "update for upload to heroku"

アプリをデプロイ

$ git push heroku master

マイグレーションファイル作成

$ heroku run rake db:migrate

参考

丁寧すぎるDocker-composeによるrails + MySQL on Dockerの環境構築

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WindowsでWSL、Ubuntu、Rubyをインストール

「Windows Subsystem for Linux上のUbuntu上のRuby」のメモです。やったのは下記。

  1. Windows 10でWSL(Windows Subsystem for Linux)を有効化してUbuntuインストール
  2. Ubuntu上でrbenvインストール
  3. rbenvを使ってRubyインストール
  4. bundlerを使ってgem(ライブラリ)インストール

ローカルにRuby環境を整えておこうと思ったのだけど、これまでよく使っていたPerlではWindows固有の問題で手間取ることがあったので、「Windows版Ruby」「Windows Subsystem for Linux上のUbuntu上のRuby」の両方を用意することにしました。これは後者の作業メモということになります。

Windows 10上にUbuntuを導入

【WSL入門】第1回 Windows 10標準Linux環境WSLを始めよう:ITの教室 - @IT」の「まずはWSLとディストリビューションをインストールしよう」の項に従って、以下を行います。

  1. [コントロールパネル]-[アプリ]-[プログラムと機能]-[Windowsの機能]ダイアログを開き、[Windowsの機能の有効化または無効化]をクリックしてダイアログを開き、そこで「Windows Subsystem for Linux」にチェックを入れる。再起動が要求されるので、再起動を実行すること。
  2. Microsoft Storeを開き、「Ubuntu」を検索してインストールする
  3. Ubuntuが起動したら、ユーザー作成のため、ユーザー名とパスワードを入力する。

実はいきなり2.をやろうとして、Ubuntuのインストールが終わらないなと思ったら、「WSLが有効になっていません」という旨の英語メッセージが表示されていました。1.からやり直せば、特に問題はありませんでした。64bitマシンで、Windows Updateも行われている(バージョン1803以降になっている)環境であれば、問題は起こらなそうに思います。

Rubyのインストール

Ubuntuにgitとrbenvを入れる

rbenvを使用してアプリケーションのRubyバージョンを選択し、開発環境と本番環境が一致していることを確認してください。rbenvをBundlerと連携させて、簡単に万全のRubyアップグレードができるようにしてください。(rbenv/README.md先頭部より、意訳)

...ということで、RubyのインストールにはRubyバージョンの管理・切り替えをしてくれるらしいrbenvを使うことにします。rbenvのインストールは、「Basic GitHub Checkout」に従うことにします。

Ubuntuにログインした状態で、まずこれに必要なgitを下記コマンドでインストールします。

sudo apt install git

sudoを使うときにはパスワードを聞かれることがあるので、その時はUbuntuインストール時に設定したパスワードを入力します。

次に、前述の「Basic GitHub Checkout」手順に従って、以下のコマンドでrbenvの最新版を取得します。

git clone https://github.com/rbenv/rbenv.git ~/.rbenv

パスを通すため、以下を実行します。前述の手順には各環境用のコマンドが列記されていますが、Ubuntu Desktop用の以下のコマンドを使います。

echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc

以下を実行します。これはUbuntu on WSL上でrbenv initした際の出力に従った作業です。

echo 'eval "$(rbenv init -)"' >> ~/.bashrc

ここまで来たら、一度exitしてUbuntuを終了し、再度スタートメニューなどから呼び出します。ここまで更新してきた.bashrcが読み込まれて、パスなどが反映された状態になります。

この時点で、rbenvはinstallコマンドを持っていません。これを使えるようにするために、Optionalとなっていruby-buildもインストールします。ruby-buildのInstallation項にあるrbenvのプラグインとしてのインストール方法に従って、以下の二行を実行します。

mkdir -p "$(rbenv root)"/plugins
git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build

UbuntuにRubyを入れる

まずRubyインストール時に必要となるパッケージを入れておきます。実際に、これをせずに後述のrbenv installを進めていこうとすると、まずコンパイラ等々がない旨のエラーが出るので、build-essentialパッケージをインストールします。

sudo apt install build-essential

また、この後インストールを進めるとコンパイル時に、SSL関連等のパッケージが必要、以下を実行するようにとの旨のエラーが出るので、これもやっておくことにします。

sudo apt-get install -y libssl-dev libreadline-dev zlib1g-dev

これでrbenvによるRubyのインストールが可能になります。以下を実行して、利用できるRubyバージョンを確認します。

rbenv install --list

表示された中で、jruby等の接頭辞がつかないものでは、2.6.32.7.0-devが新しそうでした。Ruby言語の公式サイトを見ても、2.6.3が最新版のようだったので、これをインストールすることにします。

rbenv install 2.6.3

このコマンドが成功すれば、インストール完了です。

もし--listした時点で最新版のRubyが表示されないようであれば、rbenvが古いのではないかと思います。その時は以下のように.rbenvディレクトリに移動してgit pullすることで、rbenv自体を更新すると、表示されるようになるかもしれません。

cd ~/.rbenv
git pull
cd ~

通常利用するRubyバージョンを指定する

この時点でruby -v等すると、次のようなメッセージが出ます。

rbenv: ruby: command not found
The `ruby' command exists in these Ruby versions:
  2.6.3

rbenv globalの手順に従って以下を実行することで、通常利用するRubyバージョンを指定しておきます。

rbenv global 2.6.3

以下を実行して、rubyコマンドを実行できること、2.6.3班が動作することを確認します。

ruby -v

以下のような表示が出ればOKです。

ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux]

以降、明示的に利用バージョンが指定されたディレクトリ以外では、rbenv globalで指定したバージョンのrubyが呼び出されます。

Rubyインストール以降の話

bundlerのインストール

Rubyの様々なライブラリは「GEM」と呼ばれていて、gemコマンドでインストールできるようです。しかし前に読んだように、rbenvのドキュメントでは「Bundlerと連携させ」ることを推奨していました。Bundlerがインストールされていることを確認します。以下のコマンドを実行して、インストール済みのgemを確認します。

gem list

先頭からアルファベット順でインストール済みのgemが表示されます。Ubuntu on Windows上にインストールしたRuby 2.6.3では次のような表示で、bundlerがインストール済みなのを確認できました。

*** LOCAL GEMS ***

bigdecimal (default: 1.4.1)
bundler (default: 1.17.2)
cmath (default: 1.0.0)
(以下略)

他のRubyバージョンをインストールしていてbundlerがインストールされていなかった場合は、Bundlerの「Getting started」に沿って、以下のコマンドを実行します。

gem install bundler

これでbundlerを使ってgemをインストールできます。ただしその前に、次の項も確認しておいてください。

Rubyやgemの追加時の作業

rbenvでRubyを管理している場合は、次のタイミングではrbenv rehashをします。

  • 別のバージョンのRubyを追加した
  • コマンドを提供するgemを追加した

もし前の項でbundlerをインストールした場合、このgemはbundlerbundleなどのコマンドを提供するので、上記作業が必要なタイミングです。

こうしたタイミングでは、以下を実行します。

rbenv rehash

gemのインストール

Bundlerでgemをインストールします。まず、インストール対象を記載するGemfileを作成します。bundle initを実行すると、カレントディレクトリにGemfileが作成されます。

bundle init

Gemfileには様々な情報を記載できますが、上記手順で作成した場合はデフォルトのsourcegit_sourceが記載されているので、まずは追加したいgemの指定を追記するだけで使えます。例えば下記のコマンドを実行して、nokogiriを追加します。

echo 'gem "nokogiri"' >> Gemfile

Gemfileの更新が済んだら、以下のようにbundle installを実行してgemをインストールします。

bundle install

前述のとおり、必要であればrbenv rehashも実行しておきます。

インストールを行うと、Gemfile.lockというファイルが生成されています。このファイルには、実際にインストールされたgemがバージョンなどの情報と合わせて記録されています。

参考

Windows 10でのWSLの有効化とUbuntuのインストールについては、以下を参照しました。

Ruby環境の構築では、以下を参照しました。構築実施時には、これらのドキュメントも参照し、本ページの情報が古くないかを確認してください。

この他に、以下のページを参考にしました。これらを参考に試行して、その後前述のリンク先で各ステップが何をしているのか、どれが必須でどれが不要なステップか、記述が分かれるステップはどれが正しいのかなどを確認しながらまとめなおしたのが本内容になります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby製CLIツールの起動時間を最適化する

以下のようなbinstubを作りましょう。

#!/usr/bin/env ruby
require 'bootsnap'
gem_name = 'rubocop'
exe_name = 'rubocop'
cache_dir = File.join(ENV['HOME'], '.cache', 'bootsnap', gem_name, exe_name)
Bootsnap.setup(cache_dir: cache_dir, load_path_cache: false, autoload_paths_cache: false)
load Gem.bin_path(gem_name, exe_name)

Gem.bin_path1は2つ引数を取ります。1つ目がgemの名前で2つ目が実行ファイルの名前です。
gem_name/exe_nameを編集するとrubocop以外の他のCLIツールにもこのbinstubを適用できます。

このbinstubなしの場合

% time rubocop -v
0.70.0
rubocop -v  0.76s user 0.20s system 97% cpu 0.983 total

このbinstubありの場合、初回は多少多めに時間がかかる

% time bin/rubocop -v
0.70.0
bin/rubocop -v  1.04s user 0.57s system 98% cpu 1.640 total

しかし一度実行してしまえばキャッシュが作られてるので早くなる

% time bin/rubocop -v
0.70.0
bin/rubocop -v  0.53s user 0.23s system 79% cpu 0.969 total

CLIツールのソースコードを一切変更せずbinstubつくるだけで起動時間を最適化できます。

やったね ✌('ω'✌ )三✌('ω')✌三( ✌'ω')✌

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby ターミナル上で動くヌメロンを作成してみた

ヌメロンとは

簡単にいうと数当てゲーム
今回は、コンピューターが持つランダムな数値列を当てたら勝ち

ルールについて

  1. それぞれのプレイヤーが0~9までの数字が書かれたカードを使って3桁または4桁の番号を作成する。ただし、「112」「121」といった同じ数字を2つ以上使用した番号は作れない。
  2. 先攻のプレイヤーは相手が作成したと思われる番号をコールする。相手はコールされた番号と自分の番号を見比べ、コールされた番号がどの程度合っているかを発表する。
    • 数字と位置が合っていた場合は「EAT」
    • 数字は合っているが位置は合っていない場合は「BITE」
    • 例として相手の番号が 「765」、コールされた番号が「746」であった場合「1 EAT - 1 BITE」となる。
  3. これを先攻・後攻が繰り返して行い、先に相手の番号を完全に当てきったプレイヤーの勝利となる。

ルールは Numer0n数当てゲーム から抜粋しました。

今回作成したもの

今回はコンピュータのもつ数字を当てていくスタイルを採用しました
一方的に攻めていきます

ソースコード

numelon.rb
class Numelon

  #桁数を3桁か4桁で選択できる
  def select_digits
    puts "プレイする桁数を選んでください\n3 or 4"
    @select_num = gets.to_i
    if @select_num == (3 || 4)
      return @select_num
    else
      puts "桁数が間違っています"
      select_digits
    end
  end

  #ランダムな数値列の作成
  def make_num
    program_num = [*(0..9)]
    rand_array = program_num.shuffle
    @rand_num = rand_array.take(select_digits)
  end

  #入力された数値を取得
  def enter_num
    puts "#{@select_num}桁の数字を入力してください"
    num = gets.chomp
    enter_array = num.split('').map(&:to_i)

    #数字の重複を調べる
    num_dup = enter_array.group_by(&:itself).map{ |key, value| value.count }
    num_dup.delete(1)

    #桁数が違う場合、再入力を求める
    if enter_array.length != @select_num
      puts "桁数が違います。再入力してください"
      enter_num
    #同じ数字が入力されていた場合、再入力を求める
    elsif num_dup.length != 0
      puts "同じ数字が入力されています。再入力してください"
      enter_num
    else
      check_num(enter_array)
    end
  end


  def check_num(enter_array)
    eat_count = 0
    bite_count = 0

    enter_array.each_with_index {|num, i|
      if num == @rand_num[i]
        eat_count += 1
      elsif @rand_num.include?(num)
        bite_count += 1
      end
    }

    puts "#{eat_count} eat - #{bite_count} byte"

    if eat_count == @select_num
      puts "あなたの勝ちです"
      exit
    else
      enter_num
    end

  end

end

player = Numelon.new
player.make_num
player.enter_num

実際のプレイ

ruby numelon.rb
プレイする桁数を選んでください
3 or 4
5
桁数が間違っています
プレイする桁数を選んでください
3 or 4
3
3桁の数字を入力してください
12345
桁数が違います。再入力してください
3桁の数字を入力してください
112
同じ数字が入力されています。再入力してください
3桁の数字を入力してください
789
0 eat - 2 byte
3桁の数字を入力してください
198
1 eat - 0 byte
3桁の数字を入力してください
837
0 eat - 3 byte
3桁の数字を入力してください
378
3 eat - 0 byte
あなたの勝ちです

最後に

rubyの勉強ということで今回はヌメロンを作成
本家のヌメロンは対人戦だから、コンピューターと戦わせようと思ったけど難しかったから一方的に当てるスタイルにしてみた

時間ができたらひらがなヌメロンや英単語ヌメロンを作りたい

クラス・メソッドについての理解が浅いので汚いコードになっています
もっと簡潔な記述などあればコメントで教えてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyで全探索(深さ優先探索、幅優先探索)を実装してみた

はじめに

アルゴリズム、というか競技プログラミングに関するネタです。
昨日までの自分にとって有益な情報なので、Qiitaに載せておきます。

これまで再帰を使った深さ優先探索しか理解できておらず、しかも理解が甘くて使おうとしても毎度ワタワタする始末。これではいかんと思い、以下3つのパターンについて極力シンプルな形で実装してみました。

  • 深さ優先探索(DFS, Depth-First Search)
    • 再帰による実装
    • スタックによる実装
  • 幅優先探索(BFS, Breadth-First Search)
    • キューによる実装

すべてRubyでの実装です。Ruby 2.3.3での動作を確認しています。

お題

アルファベット A, B, C からなる、3文字以下の文字列の組み合わせをすべて取得する。

このお題を、全探索で解きます。この手の全探索だと"迷路を解く"問題が定番ですが、シンプルさを重視した結果こんなお題です。

深さ優先探索

再帰による実装

文字数が3文字になるまで、現在の文字列に追加の一文字を足して同じメソッドに突っ込む、というスタイルです。停止条件におけるreturnがキモ。

個人的にはこれが一番腑に落ちるスタイル(元々これしか使えていなかったというのもあるけど)。ただ、停止条件やその他の処理が複雑になると途端にわけが分からなくなるのもこのパターンかなぁ、という気がしています。

$debug = true

def recursive_dfs(patterns, string="")
  # 文字列を配列に格納する
  patterns << string unless string.empty?

  # 停止条件: 文字列長が3文字になること
  if string.length == 3
    patterns << "<leaf>" if $debug
    return
  end

  # 分岐処理
  recursive_dfs(patterns, string+"A")
  recursive_dfs(patterns, string+"B")
  recursive_dfs(patterns, string+"C")
end

patterns = [] # 結果を格納する配列を用意しておく
recursive_dfs(patterns)
p patterns # 組み合わせの表示
# => ["A", "AA", "AAA", "<leaf>", "AAB", "<leaf>", "AAC", "<leaf>", "AB", "ABA", "<leaf>", "ABB", "<leaf>", "ABC", "<leaf>", "AC", "ACA", "<leaf>", "ACB", "<leaf>", "ACC", "<leaf>", "B", "BA", "BAA", "<leaf>", "BAB", "<leaf>", "BAC", "<leaf>", "BB", "BBA", "<leaf>", "BBB", "<leaf>", "BBC", "<leaf>", "BC", "BCA", "<leaf>", "BCB", "<leaf>", "BCC", "<leaf>", "C", "CA", "CAA", "<leaf>", "CAB", "<leaf>", "CAC", "<leaf>", "CB", "CBA", "<leaf>", "CBB", "<leaf>", "CBC", "<leaf>", "CC", "CCA", "<leaf>", "CCB", "<leaf>", "CCC", "<leaf>"]

ちなみに、停止条件(文字列長が3文字)のタイミングが明確になるように、組み合わせ結果に<leaf>という文字列を追加しています。先頭行を$debug = falseとすればこの処理は行われません(他パターンのコードでも同様)。

再帰のネストが深くなりやすい点については注意した方がいいかもしれません。

スタックによる実装

同じく深さ優先探索ですが、再帰ではなくスタックを用います。現在の文字列に一文字追加したものをスタックに積んでいきループで処理する、という構造にすることで再帰を回避しています。

そのため、再帰による実装と違ってネストが深くならないところはメリットですね。

$debug = true

def iterative_dfs(patterns)
  # スタックとなる配列を用意し、初期状態である空文字列を格納する
  stack = [""]
  until stack.empty?
    # スタックから1つ取り出す(末尾から取り出すのでpopを使う)
    string = stack.pop

    # 文字列を配列に格納する
    patterns << string unless string.empty?

    # 停止条件: 文字列長が3文字になること
    if string.length == 3
      patterns << "<leaf>" if $debug
    else
      # 分岐処理
      stack << string+"C"
      stack << string+"B"
      stack << string+"A"
    end
  end
end

patterns = [] # 結果を格納する配列を用意しておく
iterative_dfs(patterns)
p patterns # 組み合わせの表示
# => ["A", "AA", "AAA", "<leaf>", "AAB", "<leaf>", "AAC", "<leaf>", "AB", "ABA", "<leaf>", "ABB", "<leaf>", "ABC", "<leaf>", "AC", "ACA", "<leaf>", "ACB", "<leaf>", "ACC", "<leaf>", "B", "BA", "BAA", "<leaf>", "BAB", "<leaf>", "BAC", "<leaf>", "BB", "BBA", "<leaf>", "BBB", "<leaf>", "BBC", "<leaf>", "BC", "BCA", "<leaf>", "BCB", "<leaf>", "BCC", "<leaf>", "C", "CA", "CAA", "<leaf>", "CAB", "<leaf>", "CAC", "<leaf>", "CB", "CBA", "<leaf>", "CBB", "<leaf>", "CBC", "<leaf>", "CC", "CCA", "<leaf>", "CCB", "<leaf>", "CCC", "<leaf>"]

以下、処理の流れを追ってみます。

スタックには初期値である空の文字列""だけが格納されている状態です。
空文字列とは言ってもスタック自体が空というわけではないので、untilループがスタートします。
スタックから""を取り出しますが、空文字列なのでpatternsには格納しません。
スタックはこれで空っぽになりました。
""の文字列長は0なので、一文字を追加した"C", "B", "A"をスタックに積みます。
スタックの中身は"C", "B", "A"です。

スタックは空ではないため、untilループが続行します。
末尾から"A"を取り出してpatternsに格納します。
スタックには"C", "B"が残ります。
"A"の文字列長は1なので、一文字を追加した"AC", "AB", "AA"をスタックに積みます。
スタックの中身は"C", "B", "AC", "AB", "AA"です。

スタックは空ではないため、untilループが続行します。
末尾から"AA"を取り出してpatternsに格納します。
スタックには"C", "B", "AC", "AB"が残ります。
"AA"の文字列長は2なので、一文字を追加した"AAC", "AAB", "AAA"をスタックに積みます。
スタックの中身は"C", "B", "AC", "AB", "AAC", "AAB", "AAA"です。

スタックは空ではないため、untilループが続行します。
末尾から"AAA"を取り出してpatternsに格納します。
スタックには"C", "B", "AC", "AB", "AAC", "AAB"が残ります。
"AAA"の文字列長は3なので、停止条件に合致します。
スタックへの追加は行いません。
スタックの中身は"C", "B", "AC", "AB", "AAC", "AAB"です。

スタックは空ではないため、untilループが続行します。
末尾から"AAB"を取り出してpatternsに格納します。
スタックには"C", "B", "AC", "AB", "AAC"が残ります。
"AAB"の文字列長は3なので、停止条件に合致します。
スタックへの追加は行いません。
スタックの中身は"C", "B", "AC", "AB", "AAC"です。

以下略。最後にはスタックに残った"CCC"を取り出して終わります。

分岐処理での文字追加をアルファベットの逆順にしているのは、再帰による実装と結果の出力を同じにしたかったためという表示上の都合です。"A", "B", "C"の順番だとどうなるか分からない場合は実際にそう書き換えて実行してみよう。

幅優先探索

キューによる実装

今度は幅優先探索です。とは言っても、スタックによる深さ優先探索とほとんど同じで、現在の文字列に一文字追加したものをキューに積んでいく、という処理を行っています。実質的に違っているのはスタックかキューかという部分だけ、つまり積んだものを末尾から取り出すか先頭から取り出すかの違いでしかありません。

$debug = true

def iterative_bfs(patterns)
  # キューとなる配列を用意し、初期状態である空文字列を格納する
  queue = [""]
  until queue.empty?
    # キューから1つ取り出す(先頭から取り出すのでshiftを使う)
    string = queue.shift

    # 文字列を配列に格納する
    patterns << string unless string.empty?

    # 停止条件: 文字列長が3文字になること
    if string.length == 3
      patterns << "<leaf>" if $debug
    else
      # 分岐処理
      queue << string+"A"
      queue << string+"B"
      queue << string+"C"
    end
  end
end

patterns = [] # 結果を格納する配列を用意しておく
iterative_bfs(patterns)
p patterns # 組み合わせの表示
# => ["A", "B", "C", "AA", "AB", "AC", "BA", "BB", "BC", "CA", "CB", "CC", "AAA", "<leaf>", "AAB", "<leaf>", "AAC", "<leaf>", "ABA", "<leaf>", "ABB", "<leaf>", "ABC", "<leaf>", "ACA", "<leaf>", "ACB", "<leaf>", "ACC", "<leaf>", "BAA", "<leaf>", "BAB", "<leaf>", "BAC", "<leaf>", "BBA", "<leaf>", "BBB", "<leaf>", "BBC", "<leaf>", "BCA", "<leaf>", "BCB", "<leaf>", "BCC", "<leaf>", "CAA", "<leaf>", "CAB", "<leaf>", "CAC", "<leaf>", "CBA", "<leaf>", "CBB", "<leaf>", "CBC", "<leaf>", "CCA", "<leaf>", "CCB", "<leaf>", "CCC", "<leaf>"]

深さ優先探索の場合の出力結果と異なり、1文字の結果、2文字の結果、3文字の結果というように文字列長ごとに結果が並びます。これが都合良い、というケースもあるでしょうね。まぁ別のやり方使ってからソートすればいいという気もしますが。

こちらも、処理の流れを追っておきます。

キューには初期値である空の文字列""だけが格納されている状態です。
空文字列とは言ってもキュー自体が空というわけではないので、untilループがスタートします。
キューから""を取り出しますが、空文字列なのでpatternsには格納しません。
キューはこれで空っぽになりました。
""の文字列長は0なので、一文字を追加した"A", "B", "C"をキューに積みます。
キューの中身は"A", "B", "C"です。

キューは空ではないため、untilループが続行します。
先頭から"A"を取り出してpatternsに格納します。
キューには"B", "C"が残ります。
"A"の文字列長は1なので、一文字を追加した"AA", "AB", "AC"をキューに積みます。
キューの中身は"B", "C", "AA", "AB", "AC"です。

キューは空ではないため、untilループが続行します。
先頭から"B"を取り出してpatternsに格納します。
キューには"C", "AA", "AB", "AC"が残ります。
"B"の文字列長は1なので、一文字を追加した"BA", "BB", "BC"をキューに積みます。
キューの中身は"C", "AA", "AB", "AC", "BA", "BB", "BC"です。

キューは空ではないため、untilループが続行します。
先頭から"C"を取り出してpatternsに格納します。
キューには"AA", "AB", "AC", "BA", "BB", "BC"が残ります。
"C"の文字列長は1なので、一文字を追加した"CA", "CB", "CC"をキューに積みます。
キューの中身は"AA", "AB", "AC", "BA", "BB", "BC", "CA", "CB", "CC"です。

キューは空ではないため、untilループが続行します。
先頭から"AA"を取り出してpatternsに格納します。
キューには"AB", "AC", "BA", "BB", "BC", "CA", "CB", "CC"が残ります。
"AA"の文字列長は2なので、一文字を追加した"AAA", "AAB", "AAC"をキューに積みます。
キューの中身は"AB", "AC", "BA", "BB", "BC", "CA", "CB", "CC", "AAA", "AAB", "AAC"です。

中略。

キューは空ではないため、untilループが続行します。
先頭から"AAA"を取り出してpatternsに格納します。
キューには"AAB", "AAC", "ABA", "ABB", "ABC", ..., "CCB", "CCC"が残ります。
"AAA"の文字列長は3なので、停止条件に合致します。
キューへの追加は行いません。
キューの中身は"AAB", "AAC", "ABA", "ABB", "ABC", ..., "CCB", "CCC"です。

以下略。最後にはキューに残った"CCC"を取り出して終わります。

注意点

スタックによる深さ優先探索とキューによる幅優先探索は実装コードがほぼ同じですが、キューによる幅優先探索には、『探索するパターンが増えると、キューが巨大化する傾向にある』という欠点があります。

お題の通りの条件では、スタックの最大長が7なのに対してキューの最大長は27です。これくらいだとそれほど大差はありません。

ですが、たとえば文字の種類が"A"から"E"までの5種類で文字列長が8文字以下、という条件の場合だと、スタックの最大長が33なのに対してキューの最大長は390,625となります。

おわりに

シンプル化するとどれもかなりカンタンなコードになりますねぇ。
これらの中だとスタックによる深さ優先探索が一番混乱しにくいコードを書けそうだと感じました。ただ、今の所再帰による実装の方に慣れてしまっているので、分岐処理を"スタックに収められる形で書く"ことに難儀しそうな気も……。

文中の間違いの指摘や改善のためのアドバイスなど、大歓迎です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby CSV取り込み ヘッダーの上にいらない行がある場合の処理

はじめに

CSV取り込み処理を作ることって結構ありますよね?
たまに1行目がヘッダーではないCSVデータってありますよね?
あれ何なんでしょうね?
何でそんなことをするのかわからないんですけど、取り込まないといけないですよね?(使命感)

どんなデータ?

こんなデータ
image.png

実装

1、2行目をスキップして行を配列として取得するだけなら単純なんですが、
CSV.foreachと同じようにrow['ヘッダー名']でデータを取得できるようにするには一工夫が必要です。

# CSVをオープン
csv = CSV.open(
  path,
  undef: :replace,
  replace: '?'
)

# ヘッダの上に邪魔な行が2行あるので除去
csv.readline
csv.readline

# ヘッダを退避
header = csv.readline

csv.each do |row|
  # CSV.foreachと同じようなデータの取り方にパース
  row = CSV.parse_line(row.join(','), headers: header)
end

おわりに

邪魔な行があったりなかったりする場合の処理はよしなにやってください。

ブログで見たい方はこちら
Ruby CSV取り込み ヘッダーの上にいらない行がある場合の処理

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】ネストされたcontroller宛てのform_withの書き方

namespaceでnestされたcontroller(Admin::Userなど)のアクション宛てformを
form_withで記述したい場合の雛形になります。

書き方

◆ new → create

new.html.erbのviewからAdmin::UsersControllercreateアクションへ送りたい場合、htmlレベルでは

<form action="/admin/users" method="post" >
  <input type="text" name="user[name]">
</form>

こうなっていてほしいが、form_withではどう記述すればよいか?

① model: を使う方法

model:@userを指定し、url: でpathを指定します。

new.html.erb
<%= form_with model: @user, url: admin_users_path, local: true do |f| %>
  <%= f.text_field :name %>
<% end %>

② model: を使わない方法

scope::userを指定し、 url:でpathを指定します。
scope: :userによってパラメーターのuser[]を生成するようです。

new.html.erb
<%= form_with scope: :user, url: admin_users_path, local: true do |f| %>
  <%= f.text_field :name %>
<% end %>

(ちなみに、scope: :userをなくすと)

new.html.erb
<%= form_with url: admin_users_path, local: true do |f| %>
  <%= f.text_field :name %>
<% end %>

↓htmlレベルでは

<form action="/admin/users" method="post" >
  <input type="text" name="name">
</form>

user[name]ではなく、nameになっています。

◆ edit → update

同様に、edit.html.erbからAdmin::UsersControllerupdateアクションへ送りたい場合、htmlレベルでは

<form action="/admin/users/3" method="post"><input type="hidden" name="_method" value="patch" />
  <input type="text" name="user[name]">
</form>

こんな感じになっていてほしいが、form_withではどう記述すればよいか?

① model: を使う方法

model:@userを指定し、url: でpathを指定します。

edit.html.erb
<%= form_with model: @user, url: admin_user_path, local: true do |f| %>
  <%= f.text_field :name %>
<% end %>

② model: を使わない方法

scope::userを指定し、 url:でpath、method::patchを指定します。

edit.html.erb
<%= form_with scope: :user, url: admin_user_path, method: :patch, local: true do |f| %>
  <%= f.text_field :name %>
<% end %>

(ちなみに、method: :patchをなくすと)

edit.html.erb
<%= form_with scope: :user, url: admin_user_path, local: true do |f| %>
  <%= f.text_field :name %>
<% end %>

↓htmlレベルでは

<form action="/admin/users/3" method="post">
  <input type="text" name="user[name]">
</form>

<input type="hidden" name="_method" value="patch" />が無くなりました。
これにより「htmlメソッドはpatchという体裁で送っているよ」というメッセージが消えたので、railsサーバーはPOSTが届いたと認識します。(詳しくはこちら)
methodがPOST、pathがadmin_user_pathの組み合わせは存在しないので、Routing Errorになるはずです。

結論

form_withではmodel:を使えば空気を読んで自動翻訳くれるのでかなり楽。
しかし、nestされている状態とpathまでは空気読みきれないので、mode:url:のセットを使えばOK.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsのService層のクラスをauto_loadpathに含める

環境

  • Ruby 2.4.2
  • Rails 5.1.4

やりたいこと

app
- services/
- neko.rb
- - utils/
- - - hoge.rb
- - users/
- - - test.rb

みたいなserivcesディレクトリ構成があるとして、全てのディレクトリ内のファイルをオートロードパスに含めたいが、デフォルトでは出来ないので、その方法について記載します。

デフォルトでは、servicesディレクトリの直下のみがオートロードパスの対象となっています。

services.rbを編集

config/initializersディレクトリ内にserices.rbを作成します(自分の環境にはありませんでした)。

services.rbを以下のようにします。
rb
Dir.glob("app/services/**/*.rb") do |file|
require_dependency Rails.root.join(file)
end

これで全てのファイルが対象となっているかと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Haml】条件(if)に応じてClassを変更させる方法

例えば、Eventというモデルの編集ページにおいて、編集不可の要素に対して付与するis-disabledというクラスを用意したとします。

.event-panel.is-disabled

Eventがeditable?でないときのみis-disabledを付与する場合は以下のように書きます。

- event = @event
.event-panel{class:('is-disabled' unless event.editable?)}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails4でhealth_checkをミドルウェア化してヘルスチェックを行う

使用するGemは↓
https://github.com/ianheggie/health_check/tree/v2.8.0
Rails4なのでREADMEにあるとおりv2.8.0を使う。

install

Gemfileの記述

gem 'health_check', "~> 2.8"

config

config/initializers/health_check.rbに色々書いていく。
GithubのREADMEにサンプルもあるので基本はそれを編集して作る。

ルーティング

↓のように書いておくと↓↓で指定したURIからアクセスできるようになる。
ちなみにpassengerなどを使ってRailsを起動していると再起動が必要になるので注意。

config/routes.rb
  health_check_routes
config/initializers/health_check.rb
  config.uri = 'health_check'

ヘルスチェック方法

http://portal-dev.lierco.com/health_check.jsonでJsonも取れる。

↓チェック項目もパラメータで指定可能。
未指定だとstandardが使用され、DBやマイグレーションなどがチェックされる。
full or allも可能でstandardよりも項目数が多い。
何がstandardなのかといった項目も変更可能で、変更方法は↓のミドルウェア化の項目を参考に。

http://portal-dev.lierco.com/database
http://portal-dev.lierco.com/standard
http://portal-dev.lierco.com/full

ミドルウェア化

DBへ接続できないときなどはRails自体が死ぬのでヘルスチェックすらできなくなる。
そうなるのを回避するためにミドルウェア化する。

config/application.rb
    config.middleware.insert_after "Rails::Rack::Logger", HealthCheck::MiddlewareHealthcheck

このときconfig.middleware_checksをミドルウェアでチェックする項目を指定することができる。
デフォルトだとDB接続は入ってないので指定しないとミドルウェアでDB接続をチェックしてくれなくなる。

ここで指定したものとURLを比較してミドルウェアで行うか判断しているみたいなのでstandardfullなどを使う場合は注意が必要。
/health_check/(/health_check/standardと同じ)やhealth_check/allにアクセスしてDBのヘルスチェックをミドルウェアでしてもらうには例えば↓のようにする。
逆にfullだけ指定してallは指定しないとかすると挙動を変えられそう(未検証)。

config/initializers/health_check.rb
  config.middleware_checks = ['middleware', 'migrations', 'standard', 'full', 'all']
  config.standard_checks = ['database', 'migrations']
  config.full_checks = ['database', 'migrations', 'custom', 'email', 'cache', 'redis', 'resque-redis', 'sidekiq-redis', 's3']

ステータスコードを変更

ヘルスチェックの結果がエラーだった場合に返るステータスコードを変更することができる。
テキストで受け取る際とオブジェクト(XMLやJson)で受け取る場合で別に指定できる。

config/initializers/health_check.rb
  config.http_status_for_error_text = 503
  config.http_status_for_error_object = 503

動作確認

ミドルウェア化によりDBに接続できない場合でもヘルスチェックができることを確認する。

  • まずは通常通りRailsを起動して/health_checkにアクセスする
    • successの文字が確認できる
  • sudo service mysqld stopとしてDBを落として/health_checkにアクセスする
    • health_check failed: closed MySQL connectionになる
    • curlに-iオプションを付けて確認するとステータスコードが503であることも確認できる
  • DBが落ちたまま/health_check以外のページにアクセスする
    • → Railsがエラーとスタックトレースを出す
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Spacemacsはいいぞ

Spacemacs is なに?

  • Emacsディストリビューション。Spacemacsよりも軽量なものにDoom Emacsなんかがあったりする。
  • Vimの秀逸な入力インターフェイスにEmacsの強力な拡張性を併せ持つそれぞれのいいとこ取りのエディタ。

メリット

  • スペースキーを起点としたコマンドが秀逸。スペースキーを押した時点で エディタの下に次のキーの候補と何のコマンドかを表示してくれて親切。
  • デフォルトで多くレイヤーが用意されていて、.rb.py などの拡張子を初めて開く場合、最初にawesome-emacsに乗っているようなモジュールを入れるか聞いてくれるので言われるがままに入れとけばまず困らない。
  • 補完が結構効く。仕事でRubyを書いていてRubyMineでは設定が悪いのか全然補完してくれなかったものがSpacemacsでは補完してくれるようになってRubyMineやめた。Robeすごい。
  • Emacsの豊富な既存のモジュールをほぼそのまま使える。
  • themes-megapackレイヤーを入れれば有り余るほどテーマが入るので、私みたいに定期的にテーマを変えたい人にはありがたい。テーマの変更は設定ファイルからできる他にSpace T sで簡単に変更できる。

デメリット

  • 情報が少ないのでSpacemacs固有の問題にぶち当たったら解決するのが結構大変らしい
  • 立ち上げてしまえばサクサクだが、やはり素のEmacsやVimやVSCodeと比べて起動が遅い。(それでもRubyMineやその他IDEよりはずっと速い)

導入手順

私はmacOSを使っているためmacOSでの導入手順を書いておきます。その他OSの方はGithubのREADMEを参照してください。

また、すでにemacsを使っている方は~/.emacs.dのバックアップを忘れずにしてください。

$ brew tap d12frosted/emacs-plus
$ brew install emacs-plus
$ brew linkapps emacs-plus
$ git clone https://github.com/syl20bnr/spacemacs ~/.emacs.d

よく使うコマンド

  • .rbファイルを開いた状態でSpace m ' : Robe(コードナビゲーション、ドキュメント検索、オートコンプリート)の起動
  • Space p f : プロジェクト内のファイル検索
  • Space / : プロジェクト内のテキスト検索
  • Space f f : ファイル検索
  • Space T T : エディタの透過調整
  • Space g b : git blame
  • Space T s : エディタのテーマ変更
  • Space w - : エディタの水平分割
  • Space w v : エディタの垂直分割
  • Space w w : 次のウィンドウに移動
  • Space {0~9} : 指定した数字のウィンドウに移動
  • Space p t : プロジェクトツリー(Treemacs)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails herokuデプロイ時のエラーについて

rails初学者です。
herokuにアプリをデプロイしたところ、特定のページでのみエラーとなります。
試行錯誤を繰り返したのですが、自力での解決が難しいため質問させてください。

下記、「heroku logs -t」でのエラー文です。

PG::DatatypeMismatch: ERROR:  argument of WHERE must be type boolean, not type integer

LINE 1: SELECT  "users".* FROM "users" WHERE (1) LIMIT $1

: SELECT  "users".* FROM "users" WHERE (1) LIMIT $1):

app/controllers/posts_controller.rb:89:in `follows'

上記の文面から、postsコントローラーのfollowsアクションで、「WHERE句はinteger型ではなくboolean型ですよ。」との内容だと思うのですが...

修正が必要と思われる箇所はこちらです↓

posts_controller.rb
  def follows
    @user  = User.find_by(params[:id]) #ここが89行目
    @follows = Follow.where(follower_id: @user.id)
    @posts = []
    if @follows.present?
      @follows.each do |follow|
        posts = Post.where(user_id: follow.followed_id).order(updated_at: :desc)
        @posts.concat(posts)
      end
      @posts.sort_by!{|post| post.created_at}.reverse!
      if @posts.nil?
        flash[:notice]="まだ投稿がありません…"
        redirect_to("/posts/index")
      end
    else
      flash[:notice]="誰かをフォローしてみましょう!"
      redirect_to("/posts/index")
    end

説明に不足・見当違いなどございましたらご指摘いただければ幸いです。
ご回答のほど、よろしくお願い致します!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「3ステップでしっかり学ぶRuby入門」 第1~5章まとめ

はじめに

Rubyの勉強を始めて約2週間程度の超初心者です。教えていただいた本「3ステップでしっかり学ぶRuby入門」の第5章までの内容をまとめてみました。拙い文章ですみません。

第1章 プログラミング言語Rubyとは

Rubyとは

Rubyはオブジェクト指向スクリプト言語です。
オブジェクト指向とは、プログラムを「オブジェクト(もの)同士の相互作用」とみなす考え方です。
スクリプト言語とは、プログラムをテキストファイルのままで実行できる言語のことです。

Rubyプログラムを実行させる方法

大きく分けて3つあります。

①rubyコマンド-eオプションを指定してプログラムを直接書き、コマンドライン上から実行する方法

スクリーンショット 2019-05-29 10.34.27.png

②irbで対話的に実行する方法

スクリーンショット 2019-05-29 10.35.57.png

③Rubyプログラムが記述されたテキストファイルを指定して実行する方法

スクリーンショット 2019-05-29 10.46.59.png

スクリーンショット 2019-05-29 10.48.53.png

第2章 

変数

変数とはデータにつけるラベルのようなものです。
「変数名 = データ」で表され、右辺のデータが左辺の変数名に代入されます。

スクリーンショット 2019-05-28 21.18.30.png

上記は 1 と出力されます。

また、変数は順次処理によって、出力結果が上書きされていきます。

スクリーンショット 2019-05-28 21.21.41.png

上記は変数aに3が再代入されているため、3と出力されます。

数値の演算

+、 -、 *、 / を使って四則演算することができます。
足し算
スクリーンショット 2019-05-29 10.54.40.png
引き算
スクリーンショット 2019-05-29 10.55.38.png
掛け算
スクリーンショット 2019-05-29 10.55.47.png
割り算
スクリーンショット 2019-05-29 10.56.03.png

第3章

配列

配列とは複数のデータを1つにまとめたもので、[要素1, 要素2, 要素3]と表されます。
配列を使うことで複数のデータのまとまりを1つの変数で表現することができます。

スクリーンショット 2019-05-28 21.41.43.png
上記の出力結果は、[apple, banana, cherry]となります。

配列でデータをまとめ、まとめたデータは添字で取り出すことができます。
配列[添字]で表現してみましょう。

スクリーンショット 2019-05-28 22.00.46.png

上記の出力結果は、'cherry'となります。

ハッシュ

ハッシュとは複数のデータをまとめて扱うものです。
配列との最大の違いは、添字に数値ではなく文字列を利用できることです。

ハッシュは{ 'キー1' => 値1, 'キー2' => 値2, ...}のようにキーと値のペアを記述して定義します。

スクリーンショット 2019-05-28 22.18.57.png

スクリーンショット 2019-05-28 22.19.20.png

第4章

if

ifで、条件式が正しい場合に特定の処理を実行できます。

表記方法
スクリーンショット 2019-05-29 11.09.16.png


スクリーンショット 2019-05-29 4.15.41.png

出力結果は2となります。

case

case・・・複数の条件に対応する処理を実行するときに使います。

caseの他にwhen,else,endを使って記述します。
表記方法
スクリーンショット 2019-05-29 11.12.00.png


スクリーンショット 2019-05-29 5.05.00.png

出力結果は"バナナです"となります。

第5章

times

timesメソッドを使うと、同じ処理を繰り返すことができます。

表記方法
スクリーンショット 2019-05-29 9.54.47.png


スクリーンショット 2019-05-29 9.41.36.png

スクリーンショット 2019-05-29 9.42.24.png

変数は0から数値-1まで繰り返しの度に増えます。

each

eachメソッドは、それぞれの要素に対して繰り返し処理をすることができます。
timesメソッドとの違いは、繰り返しの回数を意識することなく繰り返し処理が可能なところです。

表記方法
スクリーンショット 2019-05-29 10.02.53.png


スクリーンショット 2019-05-29 10.06.44.png

スクリーンショット 2019-05-29 10.07.35.png

while

while式を使うと、条件に応じて処理を繰り返すことができます。
whileの性質上、繰り返し処理の結果、ずっと条件式がtrueにならない場合は処理が終わらなくなります。

表記方法
スクリーンショット 2019-05-29 10.18.06.png


スクリーンショット 2019-05-29 10.15.58.png

スクリーンショット 2019-05-29 10.16.28.png

まとめ

より理解を深めて、簡潔にわかりやすく書けるように練習していきます。次回は、メソッド・クラスを中心にまとめたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

みなさんが必ず使う検索機能について・・・

みなさんこんにちは☀️

検索・・・
それは誰でもしたことあるやつです。

インターネットで調べる時、メルカリで欲しいものを検索したい時など。

現代ではスマートフォンによっていつでもどこでも検索することができます。

僕がプログラミング学習しているときでも、
検索を常にしています。
今日はその検索機能をRubyではどう書くのかみていきたいと思います。

検索機能

RubyではLIKE旬を利用します。
LIKE旬とは、曖昧な文字列を検索することができることです。
これは、Whereメソッドと一緒に使います。

実行サンプルを置いておきます。

aから始まるタイトル
where(‘title LIKE(?)’, “a%”)

bで終わるタイトル
where(‘title LIKE(?)’, “%b”)

cが含まれるタイトル
where(‘title LIKE(?)’, “%c%”)

dで始まる2文字のタイトル
where(‘title LIKE(?)’, “d_”)

eで終わる2文字のタイトル
where(‘title LIKE(?)’, “_e”)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6 のちょい足しな新機能を試す26(MySQL utf8mb4 編)

はじめに

Rails 6 に追加されそうな新機能を試す第26段。 今回は、 MySQL の utf8mb4 編です。
Rails 6.0.0 では、 rails new -d mysql を実行(データベースにMySQLを指定)したときに encoding が utf8mb4 になります。

Ruby 2.6.3, Rails 6.0.0.rc1 で確認しました。Rails 6.0.0.rc1 は gem install rails --prerelease でインストールできます。

$  rails --version
Rails 6.0.0.rc1

Rails プロジェクトを作る

-d mysql を指定します。

$ rails new rails6_0_0rc1 -d mysql
$ cd rails6_0_0rc1

database.yml を確認する

yda database.yml を確認すると、 encoding が utf8mb4 になっていることがわかります。

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  ...

User の CRUD を作る

utf8mb4 を試すために、実際に User の CRUD を作ります。

$ bin/rails g scaffold User name

データベースの migration

$ bin/rails db:create db:migrate

rails server を実行してユーザーを登録する

bin/rails s を実行して、 http://localhost:3000/users/new にアクセスして User を登録します。
このとき name に ?と? を入力します。

$ bin/rails s

Rails 6.0.0rc1 では無事に登録できました。

rails600rc1.png

Rails 5.2.3 ではエラーになります。

rails523.png

蛇足ですが

name に絵文字はないだろうとか思ったりしましたが、そこはスルーで。
rails g scaffold Book title とかにした方がまだ良かったかもと思わないでもないです...。

スクリーンショットの絵文字とこの記事の絵文字の見え方が違うのは、多分 OS の(フォント?)の違いです。
スクリーンショットは Ubuntu 16.04 の Chrome で取ったものです。

試したソース

試したソースは以下にあります。
https://github.com/suketa/rails6_0_0rc1/tree/try026_mysql_utf8mb4

参考情報

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[16??日目]Ruby環境構築 等々

少し更新をサボった。

cmderをコンソールとして使う。
sinatraを触るために https://qiita.com/k-ta-yamada/items/9e35c5f8b31862267e01 
このサイトを始めたところ。ローカルアドレスから抜けられないのとルーティングとか追加のところで詰んだ。

また参考書”たのしいRuby 第6版”を読み始めた。とりあえず1から写経。
並行して"大学4年間のデータサイエンスが10時間でざっと学べる"を読んでいる。

6h位???

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

color_fieldすご

Articleモデルカラムに色を登録するカラムcolor_1color_2を追加する

color_field

jqueryのカラーピッカーとかを使って色を登録しようかなんて思っていたところRailsが提供しているcolor_fieldというものを見つけた。

これで色を選択できる!こんなかんじ↓

スクリーンショット 2019-05-29 1.27.09.png

_form.html.erb
  <tr>
    <th scope="row">吹き出し1</th>
    <td><%= f.text_field :balloon_1, size: "90x10"  %>
        <%= f.color_field :color_1 %>
    </td>
  </tr>
  <tr>
    <th scope="row">吹き出し2</th>
    <td><%= f.text_field :balloon_2, size: "90x10" %>
        <%= f.color_field :color_2 %>
    </td>
  </tr>

色の表示

<div class = "balloon1" style="background-color:<%= @article_.color_1 %>">
<div class = "balloon1" style="background-color:#b8e1f5">

スクリーンショット 2019-05-29 1.47.20.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

color_fieldとインラインでの擬似要素の表示について

吹き出しの背景色をその時の気分で色を決めたい!

変更前がこちら

スクリーンショット 2019-05-29 21.07.38.png

やること

Articleモデルに色を登録するカラムcolor_1color_2を追加する

color_field

jqueryのカラーピッカーとかを使って色を登録しようかなんて思っていたところRailsが提供しているcolor_fieldというものを見つけた。

これで色を選択できる!こんなかんじ↓

画面収録-2019-05-29-1.32.03.gif

スクリーンショット 2019-05-29 1.27.09.png

_form.html.erb
  <tr>
    <th scope="row">吹き出し1</th>
    <td><%= f.text_field :balloon_1, size: "90x10"  %>
        <%= f.color_field :color_1 %>
    </td>
  </tr>
  <tr>
    <th scope="row">吹き出し2</th>
    <td><%= f.text_field :balloon_2, size: "90x10" %>
        <%= f.color_field :color_2 %>
    </td>
  </tr>

色の表示

<div class = "balloon1" style="background-color:<%= @article_.color_1 %>">
<div class = "balloon1" style="background-color:#b8e1f5">

スクリーンショット 2019-05-29 1.47.20.png

これだとまだ吹き出しの3角の部分の色がstyle.cssで指定したままの色なので未完成です。
この吹き出しのCSSはこのようなコードでできています。

style.css
.detail .article .balloon1{
  position: relative;
  padding: 20px;
  border-radius: 10px;
  background-color: #ffb1cd99;
  width: 75%;
  height: 25px;
}

/* 三角アイコン */
.balloon1::before{
  content: '';
  position: absolute;
  display: block;
  width: 0;
  height: 0;
  left: -15px;
  top: 17px;
  border-right: 15px solid #ffb1cd99;
  border-top: 15px solid transparent;
  border-bottom: 15px solid transparent;
}

::beforeってのが擬似要素ってやつでこれのスタイル指定を別で行わなきゃいけないようです。

擬似要素ってなあに?って方はこちら↓
この方の記事がわかりやすかったです!
いろんな仕様の変更とかで:::が混じってわかりにくい記事も多いです
参考:擬似要素と擬似クラス

  <style>.balloon1::before { border-right: 15px solid <%= @article_.color_1 %>; }</style>
  <div class = "balloon1" style="background-color:<%= @article_.color_1 %>"><%= @article_.balloon_1 %></div>

スクリーンショット 2019-05-29 12.06.31.png

できた!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rubyでいいかんじにDIするhabuの紹介

TL;DR

rubyでいい感じにDIするgemを作りました。使い方を紹介します。いいなと思ったらStarください。
https://github.com/hanachin/habu

ユーザーがある。

user.rb
User = Struct.new(:name)

新しくユーザーを作るサービスがある。これはuser_repositoryに依存している。コンストラクタ(#initialize)を@Injectでアノテーションする。

new_user_service.rb
class NewUserService
  @Inject
  def initialize(user_repository)
    @user_repository = user_repository
  end

  def call(*params)
    @user_repository.new(*params)
  end
end

まずはじめに先頭でrequire "habu/setup"する。必要なソースをrequireする。

app.rb
require 'habu/setup'
require_relative 'user'
require_relative 'new_user_service'

DIコンテナを作成する。

app.rb
# Create a new container
container = Habu::Container.new

Habu::Container#[]を呼び出しuser_repositoryのつくりかたをブロックで定義する。

app.rb
# Register user_repository service by passing the block as service factory
container[:user_repository] { User }

Habu::Container#newUserServiceクラスを渡すとuser_repositoryをコンストラクタ注入する。いい感じにうごく。

app.rb
# Call Habu::Container#new to get instance
new_user = container.new(NewUserService).call("hanachin")
# => #<struct User name="hanachin">

Habu::Container#[]に渡されたブロックは引数としてHabu::Container自身をうけとる。
Habu::Container#[]をブロックなしで呼び出すと定義時に渡されたブロックを呼び出した結果を返す。

これによりnew_userの定義時にuser_repositoryを参照できる。

app.rb
# Factory block take a container as argument
container[:new_user] do |c|
  # You can get the service object by calling Container#[](service_name)
  NewUserService.new(c[:user_repository])
end

ブロックなしでよびだす、いい感じに動く。

app.rb
new_user = container[:new_user].call("hanachin")
# => #<struct User name="hanachin">

DIコンテナをHabu::Container#to_refinementsでRefinements化してusingする。
@Injectしたコンストラクタがオーバーライドされているので単に.newするだけで依存性が注入された状態で呼び出せる。
Habu::Container#new経由でインスタンス化する必要はない。

# Using container as refinements for shorthand for container.new
using container.to_refinements
new_user = NewUserService.new.call("hanachin")
# => #<struct User name="hanachin">

動機

「Ruby on Railsの正体と向き合い方」
https://speakerdeck.com/yasaichi/what-is-ruby-on-rails-and-how-to-deal-with-it?slide=59
https://youtu.be/ecpq0U4zkWE?t=1648

密結合→疎結合に移行できると良いのではみたいな文脈でLaravelのDIコンテナに言及があった。
あるとべんりそうだしつくるか〜。

DIとは

他でいい感じの記事よんでください。
http://dwango.github.io/scala_text_previews/forkme/advanced-trait-di.html#%E4%BE%9D%E5%AD%98%E6%80%A7%E3%81%AE%E6%B3%A8%E5%85%A5%E3%81%A8%E3%81%AF%EF%BC%9F

†闇の動機†

Javaの引数がないアノテーションはrubyだとインスタンス変数なのでSyntax OK

inject_annotation.rb
class C
  @Inject
  def initialize(c)
    @c = c
  end
end
% ruby -c inject_annotation.rb
Syntax OK

インスタンス変数による注釈が実装できるとPythonのデコレータの実装もできる。

Raubyをキメると気持ちいい
@InjectでRubyキメたい。
†注釈術士†になりたい。

rubyで@Injectの位置を取得する

TracePoint

まずみなさんが試すのがTracePointだと思います。

get_inject_location_by_trace_point.rb
TracePoint.trace(:line) do |tp|
  raise if File.read(tp.path).lines[tp.lineno-1].match?(/@Inject/)
end

class C
  @Inject
  def initialize(c)
    @c = c
  end
end

残念ながら取れません。

% ruby get_inject_location_by_trace_point.rb
% 

InstructionSequence

TracePointを使っても実行時に取れないということは? InstructionSequenceを覗いてみましょう。

値を返すときはgetinstancevariableがある。

% ruby --dump=insns -e '@Inject'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,7)> (catch: FALSE)
0000 getinstancevariable          :@Inject, <is:0>                    (   1)[Li]
0003 leave

返さないときはgetinstancevariableがない。

% ruby --dump=insns -e '@Inject; nil'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,12)> (catch: FALSE)
0000 putnil                                                           (   1)[Li]
0001 leave

残念ながら取れません。

RubyVM::AbstractSyntaxTree

InstructionSequenceにコンパイルされる前なら取れる。

% ruby --dump=parsetree -e '@Inject; nil'
###########################################################
## Do NOT use this node dump for any purpose other than  ##
## debug and research.  Compatibility is not guaranteed. ##
###########################################################

# @ NODE_SCOPE (line: 1, location: (1,0)-(1,12))
# +- nd_tbl: (empty)
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_BLOCK (line: 1, location: (1,0)-(1,12))
#     +- nd_head (1):
#     |   @ NODE_IVAR (line: 1, location: (1,0)-(1,7))*
#     |   +- nd_vid: :@Inject
#     +- nd_head (2):
#         @ NODE_NIL (line: 1, location: (1,9)-(1,12))*

InstructionSequenceのコンパイル時にフックを仕掛けてASTを取得する

ASTの状態ならアノテーション(と勝手に呼んでいるただのインスタンス変数)が取れることがわかりました。
コンパイルされる前に @Inject を取る方法を紹介します。

RubyVM::InstructionSequence.load_iseqを使います。

参考: YARV Maniacs 【第 13 回】 事前コンパイルへの道

.load_iseqは引数でファイル名を取ります。
次のような感じでファイル名を出力したあとRubyVM::InstructionSequence.compile_fileでファイルをコンパイルし返す処理をprependで差し込んでみます。

iseq_patch.rb
iseq_patch = Module.new do
  def load_iseq(fname)
    p fname
    RubyVM::InstructionSequence.compile_file(fname)
  end
end
RubyVM::InstructionSequence.singleton_class.prepend(iseq_patch)

pp true

フックできたようですね。

% ruby iseq_patch.rb
"/home/sei/.rbenv/versions/myruby/lib/ruby/2.7.0/pp.rb"
"/home/sei/.rbenv/versions/myruby/lib/ruby/2.7.0/prettyprint.rb"
true

ASTを取得してみます。

iseq_patch_to_get_ast.rb
iseq_patch = Module.new do
  def load_iseq(fname)
    p RubyVM::AbstractSyntaxTree.parse_file(fname)
    RubyVM::InstructionSequence.compile_file(fname)
  end
end
RubyVM::InstructionSequence.singleton_class.prepend(iseq_patch)

pp true

無事ASTがとれました。

% ruby iseq_patch_to_get_ast.rb
#<RubyVM::AbstractSyntaxTree::Node:SCOPE@1:0-593:3>
#<RubyVM::AbstractSyntaxTree::Node:SCOPE@1:0-556:3>
true

ちなみにsuperは呼べません。

iseq_patch_call_super.rb
iseq_patch = Module.new do
  def load_iseq(fname)
    super
  end
end
RubyVM::InstructionSequence.singleton_class.prepend(iseq_patch)

pp true
% ruby iseq_patch_call_super.rb
Traceback (most recent call last):
        4: from iseq_patch_call_super.rb:8:in `<main>'
        3: from <internal:prelude>:210:in `pp'
        2: from /home/sei/.rbenv/versions/myruby/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        1: from /home/sei/.rbenv/versions/myruby/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require'
iseq_patch_call_super.rb:3:in `load_iseq': super: no superclass method `load_iseq' for RubyVM::InstructionSequence:Class (NoMethodError)

ASTから@Injectされているクラスの名前を取る

なおnestingや上の階層のモジュールは考慮しないものとする。

Refinementsでおもむろに探索用のメソッドを生やすとべんり。

参考: akaza/ast_ext.rb at master · pocke/akaza

module Habu
  module AnnotationCollectorHelper
    refine(Object) do
      def traversable?
        false
      end
    end

    refine(RubyVM::AbstractSyntaxTree::Node) do
      def traversable?
        true
      end

      def traverse(&block)
        block.call(self)
        children.each { |child| child.traverse(&block) if child.traversable? }
      end
    end
  end
end

case in用のdeconstructを生やしていい感じに探索する。

module Habu
  module AnnotationCollectorHelper
    refine(RubyVM::AbstractSyntaxTree::Node) do
      def deconstruct
        [type, *children]
      end

      def each_constructor_annotations(&block)
        klass_name = nil
        inject_annotation_found = false
        traverse do |ast|
          case ast
          in :CLASS, [:COLON2, nil, klass_name], *_
            # TODO: Support namespace
          in :IVAR, :@Inject
            inject_annotation_found = true
          in :DEFN, :initialize, *_ if inject_annotation_found == true
            block.call(klass_name)
            inject_annotation_found = false
          else
            inject_annotation_found = false
          end
        end
      end
    end
  end
end

こうすると直前に出てきたクラス定義のクラス名がeach_constructor_annotationsにわたしたブロックの引数として渡される。

あとはいい感じに溜め込んでいけばよいだけ。load_iseqの中からAnnnotationCollectorのインスタンスのインスタンス変数につっこんでいく。define_methodとmethod reference operatorを組み合わせるとこういう、prependしたモジュールから他のオブジェクトのインスタンスに副作用をおこすようなモジュールが手軽にかけてべんり。

require 'habu/annotation_collector_helper'

module Habu
  class AnnotationCollector
    using ::Habu::AnnotationCollectorHelper

    attr_reader :constructor_annotations

    def initialize
      @constructor_annotations = []
      @iseq_interceptor = new_iseq_interceptor
    end

    def install
      RubyVM::InstructionSequence.singleton_class.prepend(@iseq_interceptor)
    end

    private

    def add_constructor_annotation(klass)
      @constructor_annotations << klass
    end

    def new_iseq_interceptor
      add_constructor_annotation = self.:add_constructor_annotation
      Module.new do
        define_method :load_iseq do |fname|
          ast = RubyVM::AbstractSyntaxTree.parse_file(fname)
          ast.each_constructor_annotations(&add_constructor_annotation)
          RubyVM::InstructionSequence.compile_file(fname)
        end
      end
    end
  end
end

これのinstallhabu/setupのなかで行う。アノテーションの収集は実質グローバルなのでHabu.annotation_collector経由でどこからでもアクセスできるようにしておく。

require 'habu'

module Habu
  module Setup
    class << self
      def install(annotation_collector)
        ::Habu.annotation_collector = annotation_collector
        ::Habu.annotation_collector.install
      end
    end
  end
end

Habu::Setup.install(::Habu::AnnotationCollector.new)

DIコンテナの実装

定義・参照

雑にこうした。#[]は友達。

module Habu
  class Container
    def initialize
      @factories = {}
    end

    def [](key, &block)
      if block
        @factories[key] = block
      else
        @factories.fetch(key).call(self)
      end
    end
  end
end

参照方法についてはHabu::Container#cacheを用意して常に同じインスタンスを参照できるようにしたり、メソッドも合わせて定義したりとかやり方が色々ありそう。定義の方法についてはブロック渡す一択でよいと思う。#[]以外のエイリアスを与えるのもあり。

インスタンス化

メソッド引数との一致を見る。

module Habu
  class Container
    def new(klass, &block)
      params = klass.instance_method(:initialize).parameters
      klass.new(*params.filter_map { @1 == :req && self[@2] }, &block)
    end
  end
end

コンテナ経由でのインスタンス化は面倒

直接newして手軽に使えたほうがべんりなので@Injectされてるクラスのnewを全部上書きしてコンテナに移譲していく。

module Habu
  class Container
    def to_refinements
      refinements = Module.new
      refinements.instance_exec(self) do |container|
        Habu.annotation_collector.constructor_annotations.each do |klass_name|
          klass = const_get(klass_name)
          refine(klass.singleton_class) do
            define_method(:new) do |&block|
              container.new(klass, &block)
            end
          end
        end
      end
      refinements
    end
  end
end

将来的にやりたいこと

  • @Injectされてるメソッドに関しては静的解析ではなくTracePointを使ってメソッド定義時に動的に取得したい
  • コンストラクタインジェクション以外のサポート
  • メソッド呼び出し形式での参照のサポート
  • シングルトンで参照するようなコンテナの追加

真似をしたいrubyでDIライブラリ

Habu実装したあとから調べてみると dry-container/dry-auto_inject がよさそうだた。
やっぱりcontainerに参照できるメソッドを生やすのはよさそう。API真似していきたい。
ただし個人的にはmixinしなくてもdelegate_to :containerみたいな形で移譲できるだけで十分なんじゃないかなと思う。

https://dry-rb.org/gems/dry-container/
https://dry-rb.org/gems/dry-auto_inject/

使用例

所感

  • habu gemを使うとRefinementsでいいかんじに依存性を注入できる
  • Rubyのインスタンス変数は実質アノテーション
  • Rubyではソースコード中に書かれた式がRubyVMで実行されるとは限らない
  • RubyVMで実行されないような式もASTなら取れる
  • RubyVM::InstructionSequence.load_iseqを上書きするとRubyVMがソースコードをコンパイルする前にフックできる
  • #to_refinementsでRefinementsのモジュールを動的に作って返す手法はRubyキメた感があってよい

最後に

rubyで@Injectでキメるgemを作ったので使ってみてください。いいなとおもったらStarボタンよろしくお願いします。
https://github.com/hanachin/habu

Qiitaの他にGitHubもやっているのでよかったらフォローしてください。
https://github.com/hanachin

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む