20191205のRubyに関する記事は25件です。

【Rails】Rails⇔JavaScript間で時間データを渡す方法

【はじめに】

先日、こちらの記事を書きました。
【Rails】Rails側で定義した変数をJavaScriptに簡単に渡せるgem 「gon」を使ってみた - Qiita

ただ、時間関係の変数についてはフォーマットがRubyとJavaScriptで異なってしまい、うまく動きません。

そこでRails⇔JavaScript間で時間データを渡す変換方法を調べたのでまとめてみました:relaxed:

【この記事が役に立つ方】

  • Rails⇔JavaScript間で時間データをやりとりしたい方

【この記事のメリット】

  • Rails⇔JavaScript間で時間データをやりとり出来るようになる。

【環境】

  • macOS Catalina 10.15.1
  • zsh: 5.7.1
  • Ruby: 2.6.5
  • Rails: 5.2.4

【ポイント】

1000を掛ける、割るで調整する。


【変換方法】

1.Ruby → JavaScript

t1 = Time.now
=> 2019-12-05 14:11:03 +0000

t1.to_f * 1000
=> 1575555063351.479

Ruby側でto_fメソッドを使い、1000を掛けることでミリ秒に変換。

JavaScript側でnew Date()の引数にそのミリ秒を渡す。

let d1 = new Date(1575555063351.479)

d1
// Thu Dec 05 2019 23:11:03 GMT+0900 (日本標準時)

2.JavaScript → Ruby

let d2 = new Date()

d2
// Thu Dec 05 2019 23:20:04 GMT+0900 (日本標準時)

d2.getTime() / 1000
// 1575555604.804

JavaScript側でgetTime()で拾った数値を1000で割る。

Ruby側でTime.at()の引数にその数値を渡す。

Time.at(1575555604.804)
=> 2019-12-05 14:20:04 +0000

【参考】公式リファレンスより

【Ruby】 Timeオブジェクト 単位

Time オブジェクトは時刻を起算時からの経過秒数で保持しています。起算時は協定世界時(UTC、もしくはその旧称から GMT とも表記されます) の 1970年1月1日午前0時です。なお、うるう秒を勘定するかどうかはシステムによります。

class Time (Ruby 2.6.0 リファレンスマニュアル)

【JavaScript】 Dateオブジェクト ミリ秒単位

日付や時刻を扱うことが可能な、JavaScript の Date インスタンスを生成します。Date オブジェクトは、1970 年 1 月 1 日 (UTC) から始まるミリ秒単位の時刻値を基準としています。

Date - JavaScript | MDN

【おわりに】

最後まで読んで頂きありがとうございました:bow_tone1:

特にJavaScriptと他言語は頻繁に関わってくると思いますが、言語間でエラーが出たらそもそも前提が違うという視点を持って対処していかないといけないですね:thinking:

【参考にさせて頂いたサイト】(いつもありがとうございます)

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

現場で役に立つ!最小限の修正で仕様変更に対応する方法(Ruby)

この記事は Ruby Advent Calendar 2019 第6日目の記事です。

時間がないのにどうしても仕様変更しなくてはならないとき、どんな考え方で対応しますか?ってお話です。

はじめに

この記事では、主に大規模Webアプリケーションの開発に於いて、新規開発ではなく修正や拡張を行うシーンを想定して、無駄な工数をなるべく削減すべく自分なりに考えて実践しているベストプラクティスを書いている。

要は、時間優先で修正にあたるとき、実際に自分が頭の中で考えていることを分かりやすく書いた、みたいな内容だ。

また、紹介するコードはRubyに特化したものになっているが、概念としては特定のプログラミング言語に依存しない、考え方そのものについて書いているつもりだ。

対象とする読者層

対象とする読者は、プログラミング経験1年以上、設計工程よりも実装工程をメインに担当していて、コーディングしてテストしての毎日に追われている若手プログラマ、オブジェクト指向の言葉は知ってるけど実際には現場で使いこなせてない感が残る、初級から中級へステップアップしようとしているエンジニア、俗に言う IT土方 、といったところか。

特に、オブジェクト指向やアーキテクチャに関する書籍を読んで何となく理解したけども、実際に仕事でそれを活用できていない、具体的な実装例を見てみたい、といった人をターゲットとしている。

この記事を読んで「なるほど、そうすればいいのか」といった気づきが得られれば幸いだ。

コードベースの紹介

では始めよう。

とある大規模システムにて、何らかの外部APIの呼び出しをラップするメソッドがあったとしよう。

ソースコードは以下のような感じだ。

the_api.rb
class TheApi
  # APIを呼び出す
  # @return [String] 正常なら 'success'、エラーなら 'fail' を返す
  def self.call(param1, param2)
    ret = 'success'

    api = ApiClient.get_instance

    # 外部APIを呼び出すような処理を想定
    api.call [param1, param2]

    # 処理結果は last_error で得られる。0 以外ならエラー。
    if api.last_error != 0
      ret = 'fail'
    end

    ret
  end
end

戻り値は String で、呼び出し側で成功か失敗かの判定を行なっている。

呼び出し側
# API呼び出し
ret = TheApi.call(product_id, base_date)
if ret == 'fail'
  # エラー発生。
  raise 'API呼び出しでエラー発生'
end

外部API自体の詳細は不明で、 ApiClient クラスはサードパーティ製であり自由に修正できない部分、ブラックボックスであるとしよう。

システム全体では、上記のようにこのメソッドを利用している箇所が 50箇所ほど あるとする。どうやらコピペして作られたようだ。

コードの善し悪しはさておき、とりあえずこの様な状態のコードベースがあり、あなたはこのシステムの仕様変更を担当することになった。

仕様変更お願いします!

チームリーダーより、仕様変更の依頼がきた。

仕様変更の概要

  • 外部APIの呼び出しがタイムアウトした場合に、リトライする様にしたい。
  • ただし、50箇所ある呼び出し元のうち、まずは1箇所だけでタイムアウトの対応をいれたい。将来的には10箇所ぐらいまで増えるかも。
  • 外部APIの仕様書によると、api.lasr_error3 の場合に、タイムアウトを意味すると書かれている。
  • チームリーダー「やり方は任せるから、いい感じの実装を頼むよ。でも時間は余りない。 今日中に頼む

なるほど、なるほど。
要件自体は簡単なハナシだ。

以下のようなイメージだ。

修正イメージ
class TheApi
  # APIを呼び出す
  # @return [String] 正常なら 'success'、エラーなら 'fail'、
  #                  タイムアウトなら 'timeout' を返す
  def self.call(param1, param2)

    # 省略

    if api.last_error == 3
      ret = 'timeout'
    elsif api.last_error != 0
      ret = 'fail'
    end

    ret
  end
end

# 呼び出し側
ret = TheApi.call(product_id, base_date)
#if ret == 'fail'  # 判定方法を変更
if ret != 'success'
  # エラー発生。

  if ret == 'timeout'
    # TODO: タイムアウトなのでリトライする!
  end
  raise 'API呼び出しでエラー発生'
end

よっしゃ、イッチョウあがり!!

いや、待て待て。

このメソッドの呼び出し箇所は他にもあと 49箇所 あるぞ。

現状では、タイムアウトだろうと他のエラーだろうと全部 'fail' を返している。

呼び出し側では ret == 'fail' で判定しているため、タイムアウトなら時に 'timeout' を返してしまうと、エラーとして処理されなくなくなってしまう(以下の部分)。

[再掲]呼び出し側
# API呼び出し
ret = TheApi.call(product_id, base_date)
if ret == 'fail'  # 戻り値を変更するとここを全部変更しないといけない 
  # エラー発生。
  raise 'API呼び出しでエラー発生'
end

さて、どうしたら良いだろうか。

ちょっと考えてみてほしい。

まぁ、正攻法でいくなら、戻り値を String ではなく適切なクラスに変更するべきだろうな。
処理の結果をちゃんと表現できるようなクラスを定義してそのインスタンスを返す、とか。

でも 「時間は余りない、今日中に頼む」 てことなので、そこまでやる時間がなさそうだ。
戻り値を変更してしまうと、50箇所を全て変更しないといけない。
とーぜん、変更した箇所は全てテスト(動作確認)しないといけない。

これ、アカンやつやん。。。

おっと書き忘れていたが、 現在の時刻はなんと夜の9時 だ。

働き方改革とかゆーやつは一体何やったんや。笑

やばい、今日は帰れないかも!

こんな時間に急ぎのタスクを振ってくるなんて、エゲツない話だと愚痴の一つでも言いたくなるが、他のメンバーも全員残ってるし作業を振られているようだ。何かシステムトラブルでもあったのだろうか。

とにかく今は解決策を考えよう。

文句を言うのは任された仕事をやり遂げてからだ。

修正案1 グローバル変数を使う

メソッドの呼び出し箇所を変更せずに対応する方法としてまず思いつくのは、引数や戻り値とは別のところに結果を代入する方法だ。

the_api.rb
if api.last_error == 3
  $is_timeout = true
else
  $is_timeout = false
end

こんな感じにグローバル変数としてフラグを作ってしまって、タイムアウトかどうかで真偽値をセットし、呼び出し元から参照すれば良い。

めちゃくちゃ簡単に対応できそう!よっしゃー!

ごめんなさい。

ウソです。冗談です。

実際、例えばC言語の組み込み系プログラムでもない限り、今どきのWebシステムで現実にこんな事するヤツいたら 一発レッドカード だろう1

良い子は真似しないように。

グローバル変数とまではいかなくとも、Webならセッション変数に入れたり、せめて Thread.current を使ってスレッドセーフにしたいところだが、それでもどこからでもアクセス出来る変数という点では本質的に同じなので、極力使うべきでない。

この方法は超簡単な反面、ソフトウェアの構造を一瞬で ウンコに 複雑にしてしまう諸刃の剣であり、 最後の手段 、ぐらいに捉えておこう。

修正案2 引数追加

次はちゃんとした案です。

引数を追加し、デフォルト値を設定しておけば、呼び出し元を変更せずに済むよね。

the_api.rb
class TheApi
  # APIを呼び出す
  # @param param1
  # @param param1
  # @param detect_timeout [Boolean] タイムアウトを検出するかかどうか
  # @return 正常終了: 'success'
  #         エラー時: 'fail'
  #         タイムアウト時: 'timeout' ただし detect_timeout に true を指定した場合のみ
  def self.call(param1, param2 detect_timeout = false)
    ret = 'success'

    api = ApiClient.get_instance

    api.call [param1, param2]

    if detect_timeout && api.last_error == 3
      ret = 'timeout'
    elsif api.last_error != 0
      ret = 'fail'
    end

    ret
  end
end

簡単で分かりやすい方法だ。

この考え方は Ruby だけでなく、他のメジャーな言語でも使える一般的な方法なので、 必ず覚えておくべきイディオム と言ってもいいだろう。

しかし何らかの理由でこの方法が適用できない場合もある。

引数が既にオプション引数だらけで、追加したくない場合や、可変長引数になっていて特別な意味を持つ引数を追加できない場合だ。

可変長引数の例
# @param APIに渡すパラメータ
def call(*params)
  # 省略
end

無理やり追加できなくもないが、スマートではない。できればやりたくない。

こういったケースにもスマートに対応できる方法はないだろうか。

※ここからは call() メソッドの引数が可変長だった場合について考察していく

修正案3 ブロックを渡してエラー処理の仕方を追加する

引数を変更しない方法として、Ruby ならではのブロックを活用する方法を考えてみよう2

the_api.rb
class TheApi
  # APIを呼び出す
  # ブロックを渡した場合は、エラーコードを引数にブロックが呼ばれる
  # @param params [Array] APIに渡すパラメータ
  # @return 正常終了: 'success'
  #         エラー時: 'fail'
  def self.call(*params)
    ret = 'success'

    api = ApiClient.get_instance

    api.call *params

    # 処理結果は last_error で得られる
    if api.last_error != 0
      ret = 'fail'
    end

    if block_given?
      # ブロックがあるときはエラーコードを引数にして呼び出す
      yield api.last_error
    end

    ret
  end
end
呼び出し側
timeout_flag = false  # タイムアウトフラグ
ret = TheApi.call(product_id, base_date) do |error_code|
  if error_code == 3
    timeout_flag = true  # フラグを立てる
  end
end

if timeout_flag
  # TODO: タイムアウトなのでリトライする
elsif ret == 'fail'
  # エラー発生。
  raise 'API呼び出しでエラー発生'
end

どうだろう。

ちょっとムリヤリ感があるかな〜

引数を追加せずに済んだが、デメリットも多い。

  • 呼び出し側ではエラー処理の方法が2つになってしまい、複雑度が上がっている
  • TheApiクラスはせっかくAPIの処理をラップしていたのに、崩れてしまっている。呼び出し元がAPIの戻り値の仕様を知っている必要があり、開放閉鎖の原則に反している
  • 今後、もっと適切な方法でブロックを使いたくなった時に、この仕組みをコールバック関数で置き換えるとなると相当な変更量になってしまう

今回は却下かな〜

もっといい方法ないですかね。あ、せや、終電の時間も一応調べておこう。

修正案4 戻り値に特異メソッドを付与

これも Ruby ならではの方法。
Java や C# などの静的型付け言語ではこんな発想自体あり得ない3

the_api.rb
class TheApi
  # APIを呼び出す
  # @param params [Array] APIに渡すパラメータ
  # @return [String] 正常終了: 'success'
  #                  エラー時: 'fail' さらに is_timeout? という特異メソッドが定義されています
  def self.call(*params)
    ret = 'success'

    api = ApiClient.get_instance

    api.call params

    # 処理結果は last_error で得られる
    if api.last_error != 0
      ret = 'fail'

      # タイムアウト対応のための特異メソッドを定義
      ret.define_singleton_method(:is_timeout?) do
        api.last_error == 3
      end
    end

    ret
  end
end
呼び出し側
ret = TheApi.call(product_id, base_date)

if ret == 'fail'
  # エラー発生。
  if ret.is_timeout?
    # TODO: タイムアウトなのでリトライする
  end
  raise 'API呼び出しでエラー発生'
end

これなら、呼び出し元のエラー処理が分散することもないし、APIの仕様ラップをキープ出来ているし、引数やブロックの追加も必要ない。

変更量も少ないし、何より、呼び出し元の ret.is_timeout? が非常にシンプルで分かりやすい!4

やはり処理の結果は戻り値で戻すのが自然に感じるし、コールバックや出力用の引数で渡すのは言語仕様上 仕方ないとしてもやはり違和感が残る。

これぞ Ruby の柔軟性って感じ!5

ただし乱用は禁物。俗に言う「黒魔術的」な手法なので、慣れていない人6が見ると気持ち悪く見える。僕もあまり使う機会は少ないが、実際にはハッシュに要素を追加するぐらいの(JavaScript的な)感覚なので、そこまで弊害はなさそうに思う(初めて見たという人は感想をコメントください)。

修正案5 別のメソッドを追加する

現状の TheApi.call はそのまま置いといて、 TheApi.call_ex みたいな、別のメソッドを作ってしまって、そっちで新しい仕様に対応しようという考え方。

the_api.rb
class TheApi

  # APIを呼び出す(詳細なエラーを返すバージョン)
  # @param params [Array] APIに渡すパラメータ
  # @return [Integer] 0: 正常終了、それ以外はエラー(3: タイムアウト)
  def self.call_ex(*params)

    api = ApiClient.get_instance

    api.call *params

    # last_error をそのまま返す
    api.last_error
  end

  # APIを呼び出す(従来バージョン)
  # @param params [Array] APIに渡すパラメータ
  # @return [String] 正常終了: 'success'
  #                  エラー時: 'fail'
  def self.call(*params)

    # 残り時間によっては実装は変更しないままいくケースも考えられるが、
    # これぐらいなら頑張ってリファクタリングしてほしい。

    # 最適化するならこんな感じかな〜
    call_ex(*params) == 0 ? 'success' : 'fail'
  end

end

オブジェクト指向設計としては、個人的にはこれが一番良さそうに思うが、メリットデメリットを整理してみよう。

メリット

  • 既存のメソッドを残しつつ、新しい仕様にも対応できている。
  • 今後タイムアウト以外のエラーコードにも対応したくなった時でも変更しなくて良い。ただし、そんな事が実際あるのかどうかは未確認。もしかしたらこのAPIは廃止になるかもしれない。

デメリット

  • 呼び出し元でどちらのメソッドを使うべきか意識して使い分ける必要がある。考慮しないといけないことが一つ増えている。
  • 外部APIの仕様をラップしていたはずが、崩れてしまっている。すなわち、APIの結果がどうだったか判定するには外部APIの仕様を分かっている必要がある。対策としては、呼び出し側がAPIの結果を表す専用のクラスを定義(下記)し、それを戻り値にすれば更に良くなりそうたが、そこまでやる時間があるかどうか。
the_api_result.rb
# TheApi の呼び出し結果を表す
class TheApiResult
  attr_reader :error_code

  def initialize(error_code)
    @error_code = error_code
  end

  def fail?
    @error_code != 0
  end

  def timeout?
    @error_code == 3
  end

  def success?
    !fail?
  end
end

時間とのトレードオフとなると、この案は意外にも中途半端な評価になるように思う。

ガッツリとリファクタリングするなら、もっと考慮すべき事(クラスメソッドをやめて、継承可能にする、テストコードを書くなど)がたくさんあるし、時間のあるときによく考えて設計したいところ。

結論

今回の状況では、修正案4 の特異メソッドでの対応が最もお手軽で、バランスの取れた方法と判断。

これで今日もなんとか家に帰れそうだぞ!

今後もしリファクタリングするにしても、以下の様なことを確認してから検討すべきだな。

  • このAPIの利用は今後も続くのかどうか
  • 続く見込みなら、さらに仕様変更がありそうかどうか
  • ありそうなら、どんな変更を想定すべきか、どの程度か

まとめ

今回のように、(とくに大規模な開発では)純粋な設計の良し悪しだけでなく時間的な制約やチームの状況によって最適解は異なる。

本来であれば、APIの結果を 'success' 'fail' で返すなんて稚拙な設計を改めるべきであるが、現場ではそうも言ってられない7

新規に設計する仕事よりも、既に構築されて運用中のシステムがあり、設計がイケてなくても、それを修正していくような仕事の方が圧倒的に多いのだ。

良くないコードをキレイにしたい気持ちはよく分かるが、それには時間(工数)がかかる。

つまり 費用対効果 だ。

今回の例ではほんの数行の小さなメソッドで、仕様変更の内容もたかが知れているが、実際の開発の現場ではもっと大変なことがいっぱい起こっている。そんな事ないという人は、気づいていないだけだ。本当はもっと様々な、ディスプレイの中と外の両方の状況を考慮してより良い方法を探し続けていくべきなんだ。

規模が小さいソフトウェアであっても、将来的に大きくなるのを見越しているなら考え方は同じ。

安易な考えでガチャガチャ修正しているようでは、家に帰れません。笑

ちょっとした変更であっても、複数の修正案を出して検討し(できれば誰かに相談し)、最もバランスのとれた解をチーム全体で模索することが大切だ。より良い実装で、より早く仕事が終わるのなら絶対そっちのほうがいい。

ソフトウェア開発のお仕事とは、 そういった状況判断の連続 なのだ8

現場からは以上だ。お疲れ様でした!!


  1. 現実にはこれぐらいで出入り禁止になることはないだろうけど、要はそれぐらいヤバイ方法ってことだ。 

  2. Ruby 以外の言語ではコールバック関数を引数に追加するようなイメージが近い。 

  3. 僕も実際にはじめてみたときは衝撃的すぎて度肝を抜かれた。笑 

  4. 冒頭の修正イメージで掲載したコードに一番近い。僕は「こんな風に書けたらいいな」という発想で解決策を模索することが多いと思う。Ruby ならそれが実現しやすい。 

  5. この特異メソッドの使い方を紹介したくてこの記事を書いたのだ! 

  6. 特に Java や C# などの静的型付け言語から来た人 

  7. Ruby で開発しているようなハイスキルエンジニアならこんなことあり得ないのかもしれないが。 

  8. 将棋や麻雀などのゲームをやっているときの思考に近いように思う。サッカーも近いのかも。下手くそだけど。 

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

roo gemを使ってExcelからデータを読み込む

こんにちは、マイナビでエンジニア兼マネージャーをしている柴垣と申します。
マイナビ学生の窓口というメディアを担当しています。

きっかけ

マイナビ学生の窓口というメディアは大学生に対してキャリアのきっかけを届ける記事がたくさんあります。
ある日ディレクターから、「記事のカテゴリを更新して」というチケットが届きました。
対象の記事はidで指定されていて、現在のカテゴリ、変更後のカテゴリが記載されていました。
その数、約5,000
最初は気づかずに、1行ずつ、手作業で、マッピング用のHashを作っていましたが、その数に気づいた時、これはあかん!となり、excelをcsvに変換してマッピング用のHashを作るtaskを作成しようと思っていました。
そこへ、あるアドバイスが
「xlsxなどから読み込んで処理するなら roo gem を使えば、ある程度自動化できそうです。」
ありがとうございます!

rooを使う

rooとは

https://github.com/roo-rb/roo
Excelやその他類似のファイルを読み込むためのgemです

インストール

githubのREADMEにあるようにGemfileに追記します

Gemfile
gem 'roo', '~>2.8.0'

bundle installします。
現時点では2.8.2がインストールされました

Excelファイルを読み込む

ではExcelファイルを読み込みます。読み込むExcelファイルをnewの引数に指定します。

load_excel.rake
excel = Roo::Excelx.new('Excelのファイルパス')

Sheetを指定する

Sheetの指定は先ほど作成したRoo::Excelxのインスタンスにsheetメソッドで指定します。

load_excel.rake
sheet = excel.sheet('Sheet名')

指定したカラムのデータを読み込む

Sheetを指定したら、データを読み込みます。
データを読み込むメソッドはいくつかあるようですが、わたしはparseメソッドがオススメです。
ヘッダーも取り除かれます(必要な場合はheaders:trueを指定)、取得したいカラムの文字列を指定するだけです。
ExcelのSheetが以下のようになっていたとします。

id old_category_name new_category_name
1 カテゴリー1 カテゴリー2
2 カテゴリー1 カテゴリー3

そのときの指定方法は以下のようになります。

load_excel.rake
rows = sheet.parse(id: 'id', old_cname: 'old_category_name', new_cname: 'new_category_name')

rowsを出力することで正しく読み込めていることがわかります

load_excel.rake
rows.each { |row| puts row.inspect }
=> {:id=>1, :old_cname=>"カテゴリー1", :new_cname=>"カテゴリー2"}
{:id=>2, :old_cname=>"カテゴリー1", :new_cname=>"カテゴリー3"}

とてもカンタンですね!

番外編

今回はカテゴリの更新ということで、oldとnewが同じ記事のIDは配列にまとめる必要がありました
例えば

id old_category_name new_category_name
1 カテゴリー1 カテゴリー2
2 カテゴリー1 カテゴリー3
3 カテゴリー1 カテゴリー2
4 カテゴリー1 カテゴリー2
5 カテゴリー1 カテゴリー3

となっていた場合、期待する結果は以下のようになります。

[
  {old_cname: 'カテゴリー1', new_cname: 'カテゴリー2', ids: [1, 3, 4]},
  {old_cname: 'カテゴリー1', new_cname: 'カテゴリー3', ids: [2, 5]}
]

最初は、Array#findを使って、old_cname, new_cnameが結果の配列に存在していたらidsに追加という処理にしていました。
すると、レビュー時にまた神の声が聞こえてきました。

基本的にはコンテナオブジェクト(ArrayやHashなど)はHashのキーにしない方が良いのですが、ここは Array#find ではなく〜(略)

教えていただいた内容はold_cnamenew_cnameの配列をHashのkeyにして、そのvalueにidを突っ込んでいけばよいということでした。

そのために、まずHashのデフォルト値を指定します

output = Hash.new{|h, k| h[k] = []}

こうすることで、デフォルト値が[]になりました
あとはもう突っ込んでいくだけです

rows.each do |row|
  output[[row[:old_cname], row[:new_cname]]] << row[:id]
end

ouptput
=> {["カテゴリー1", "カテゴリー2"]=>[1, 3, 4], ["カテゴリー1", "カテゴリー3"]=>[2, 5]}

結果がHashになったことにより、カテゴリーの取得は少し工夫が必要で

output.each.each do |(old_cname, new_cname), ids|
end

とすることでold_cname, new_cname, idsそれぞれの変数に代入ができます

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

Rails 初学者のアソシエーションの解説

has_manyとかいまいちわからない人向け!

これがわかる人は不要 Rails ガイド

1.has_manyとbelongs_toはセットではない。一緒に使うことが多いが独立して使うことができる。
2.メソッドを理解することが重要!

例えば
Author(著者)テーブルとBook(本)テーブルがあったとし、その二つを関連づけたい時。

class Author < ApplicationRecord
has_many :books, dependent: :destroy
end

class Book < ApplicationRecord
belongs_to :author
end

上記の記述で下記のような連携ができるらしいのだが、いまいちどのような仕組みなのかわからなかった、、

@book = @author.books.create(published_at: Time.now)

こう言う時は一つづつ分解して考えるといい
authorはオブジェクト
booksはメソッド
createもメソッド

createがメソッドなのはなんとなくわかるのだが、booksは??

もしhas_many: booksがなければメソッドにはならない。has_many: booksを追加したことによりbooksメソッドができた。ここでは表記されていないが実はhas_manyはbooks()だけでなく他のメソッドの使用も可能にしている。

そもそもメソッドってなんだっけ??
Rubyでよく見かけたこんな感じのやつです。引数を入れるとreturnで値を返す。

def index
 @products = Product.all
end

booksはこのようなメソッドであり、returnで返ってきた値が反映される。

なので整理するとauthor.books.createはbooksメソッドの引数がauthorと連携して、それにcreateメソッドが対応していると言うことです。

テキストに打ち込み説明するのは難しいですが、私自身も今理解しているところなので何かご指摘などございましたらよろしくお願いします。

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

rails-tutorial第10章

modelだけ単数形の理由

modelは型に当たるので、それが複数形というのはちょっとということらしい。

edit updateアクションを実装しよう

まずはeditアクションをコントローラに実装

app/controllers/users_controller.rb
class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      log_in @user
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end

  def edit
    @user = User.find(params[:id])
  end

  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end
end

privateについて

Rubyはクラスメソッドのオーバーライドなどもでき、外側からメソッドを変更される恐れがある。
そのため、privateを宣言しそれ以下に定義したメソッドに関しては外側からアクセスできないようにしている。これで勝手にadminユーザーを作られたりなどが防げる。

編集フォームのviewを生成する。

app/views/users/edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user) do |f| %>
      <%= render 'shared/error_messages' %>

      <%= f.label :name %>
      <%= f.text_field :name, class: 'form-control' %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>

      <%= f.submit "Save changes", class: "btn btn-primary" %>
    <% end %>

    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="http://gravatar.com/emails" target="_blank">change</a>
    </div>
  </div>
</div>

これはnewアクションのviewと同じなのに、なんでupdateアクションに飛ぶの??

これはnewの場合は全く新しいインスタンスなのに対して、editの場合は、すでにDBに保存されたもので中身もあるインスタンスだから。

この違いがform_forで生成されるhtmlに影響を与えているから。

$ rails console
>> User.new.new_record?
=> true
>> User.first.new_record?
=> false

これは上記のようにrailsでtrueの時はpostリクエストを、falseの時はpatchリクエストを送るように判断するから。

updateアクションの実装

app/controllers/users_controller.rb
class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      log_in @user
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end

  def edit
    @user = User.find(params[:id])
  end

  def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      # 更新に成功した場合を扱う。
    else
      render 'edit'
    end
  end

  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end
end

もう一度update_attributesメソッドを確認しよう

update_attributesとは、Ruby on Railsのモデルに備わるメソッドで、

モデルオブジェクト.update_attributes(キー: 値, キー: 値 … )
のようにHashを引数に渡してデータベースのレコードを複数同時に更新することができるメソッドです。

データベースへの更新タイミングは、update_attributesメソッドの実行と同時で、validationも実行されます。

編集失敗時のテストを書こう

$ rails generate integration_test users_edit

test/integration/users_edit_test.rb
require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "unsuccessful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    patch user_path(@user), params: { user: { name:  "",
                                              email: "foo@invalid",
                                              password:              "foo",
                                              password_confirmation: "bar" } }

    assert_template 'users/edit'
  end
end

失敗時のコードは実装済みなのでテストは成功する。

次は編集に成功した時のテストを書こう

test/integration/users_edit_test.rb
require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "successful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    name  = "Foo Bar"
    email = "foo@bar.com"
    patch user_path(@user), params: { user: { name:  name,
                                              email: email,
                                              password:              "",
                                              password_confirmation: "" } }
    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name,  @user.name
    assert_equal email, @user.email
  end
end

今の時点では成功時の処理を書いていないので、テストは失敗する。

@user.reloadはユーザーの情報をDBの情報と同期するというもの。

具体的にupdateアクションを実装する

app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end
  .
  .
  .
end

flashとredirectを実装したので、テストが通りそうに思えるが、これでもまだ失敗してしまう。
それはテストコードで書いたパスワードが空のためだ。

パスワードには、空ではなく、さらに6文字以上というバリデーションを設定しているため、それに引っかかってしまったのである。

この状態だと、名前とemailを変えるだけなのに、passwordを毎回入力しなければいけなくなってしまう。

これを解決するにはallow_nil: trueを指定してあげると良い。

app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
  .
  .
  .
end

has_secure_passwordでは (追加したバリデーションとは別に) オブジェクト生成時に存在性を検証するようになっているため、空のパスワード (nil) が新規ユーザー登録時に有効になることはありません。(空のパスワードを入力すると存在性のバリデーションとhas_secure_passwordによるバリデーションがそれぞれ実行され、2つの同じエラーメッセージが表示されるというバグがありましたが (7.3.3)、これで解決できました。)

つまり、allow_nilを指定してもオブジェクト生成時にはhas_secure_passwordで存在性のバリデーションをしてくれるので安心である。

これでテストが通る。

認可について

今の状態だと、url直打ちでログインしてないのにuser_pathに入れたりなどが起こってしまう。

そのため、適切なユーザーでないとそのページにアクセスできないようにしたい。

beforeフィルターを使ってユーザーログインを要求する。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  .
  .
  .
  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end

    # beforeアクション

    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end
end

before_action :logged_in_user, only: [:edit, :update]

これは userコントローラのeditメソッドとupdateメソッドを呼び出す前にlogged_in_userメソッドを呼び出してねーって意味。

ここで、createアクションを直打ちでしたらエラーになるんじゃね?って思うでしょ?

でも、resourcesで /usersになっているため直打ちしてもindexアクション呼び出してーってなるから大丈夫。

いや、でもshowアクションなんかは直打ちでアクセスできちゃうぞ?
これどうするんだ?

beforeフィルターをした時の注意点

before_フィルターをすると、今まで通っていたテストが失敗してしまう。
理由はテスト時にedit,updateアクションを呼び出すときにログインを要求するようになったからだ。

これを解決するには、テスト時のユーザーに事前にログインさせる必要がある。

test/integration/users_edit_test.rb
require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "unsuccessful edit" do
    log_in_as(@user)
    get edit_user_path(@user)
    .
    .
    .
  end

  test "successful edit" do
    log_in_as(@user)
    get edit_user_path(@user)
    .
    .
    .
  end
end

これでテストは通るようになる。

ただ、今度はbefore_actionが万が一コメントアウトされたときにテストで検知しないといけなくなってしまった。

これを解決していこう。

これはuser_controller_testに書いていく。

test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "should redirect edit when not logged in" do
    get edit_user_path(@user)
    assert_not flash.empty?
    assert_redirected_to login_url
  end

  test "should redirect update when not logged in" do
    patch user_path(@user), params: { user: { name: @user.name,
                                              email: @user.email } }
    assert_not flash.empty?
    assert_redirected_to login_url
  end
end

このテストはログインしてない状態でedit,updateアクションを呼び出そうとすると、フラッシュメッセージが表示され、さらにログインページにリダイレクトされてますよね?ってテスト。

これを書いておくことで、万が一before_actionが消されるとテストが失敗するようになるので安心だよねーって話。

ただ、今の状態だと、ログインしてさえいれば他人の情報を変更できるという状態。
これを解決するにはどうすれば良いか?

このテストを書くにあたって、2人以上のサンプルデータがないとダメなので、fixtureにデータをたす

test/fixtures/users.yml
michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>

archer:
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>

次にコントローラの単体テストを書く

test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user       = users(:michael)
    @other_user = users(:archer)
  end
  .
  .
  .
  test "should redirect edit when logged in as wrong user" do
    log_in_as(@other_user)
    get edit_user_path(@user)
    assert flash.empty?
    assert_redirected_to root_url
  end

  test "should redirect update when logged in as wrong user" do
    log_in_as(@other_user)
    patch user_path(@user), params: { user: { name: @user.name,
                                              email: @user.email } }
    assert flash.empty?
    assert_redirected_to root_url
  end
end

この状態だとテストが落ちてしまう。

じゃあどうすれば?

before_actionを追加しよう

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def edit
  end

  def update
    if @user.update_attributes(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end
  .
  .
  .
  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end

    # beforeアクション

    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

    # 正しいユーザーかどうか確認
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless @user == current_user
    end
end

ここで気をつけなければいけないのは、
before_actionには順序があるということだ。

上から順番に実行されるので、ログインしている、かつ、正しいユーザーか?という順序で実行させる。

また、def logged_in_userでcurrent_userがいることは確定しているので、
無駄な処理を書かずに、params[:id]から取得したユーザーとcurrent_userを比較すれば良い

これでテストは通る。

一応リファクタリングもしておこう

unless @user == current_user

この部分を綺麗にしたい。

app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーをログイン
  def log_in(user)
    session[:user_id] = user.id
  end

  # 永続セッションとしてユーザーを記憶する
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

  # 渡されたユーザーがログイン済みユーザーであればtrueを返す
  def current_user?(user)
    user == current_user
  end

  # 記憶トークン (cookie) に対応するユーザーを返す
  def current_user
    .
    .
    .
  end
  .
  .
  .
end

ヘルパーメソッドを定義したので、

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def edit
  end

  def update
    if @user.update_attributes(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end
  .
  .
  .
  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end

    # beforeアクション

    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

    # 正しいユーザーかどうか確認
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless current_user?(@user)
    end
end

redirect_to(root_url) unless current_user?(@user)というように書き換えることができる。

フレンドリーフォアーディング

親切なリダイレクトを作ろう、もともとアクセスしたかったページにリダイレクトしてあげるようにしようということ。

まずは統合テストを書いていこう。

test/integration/users_edit_test.rb
require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    log_in_as(@user)
    assert_redirected_to edit_user_url(@user)
    name  = "Foo Bar"
    email = "foo@bar.com"
    patch user_path(@user), params: { user: { name:  name,
                                              email: email,
                                              password:              "",
                                              password_confirmation: "" } }
    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name,  @user.name
    assert_equal email, @user.email
  end
end

このようにuser_editが成功するストーリーに追加してあげる。
このままだと、ログイン後userページに飛んでしまうため、テストは失敗する。

次は、

ユーザーがもともと行きたかったページを覚えていたらそのページにアクセスし、覚えていなかったら普通のユーザーページにアクセスできるようなヘルパーメソッドを定義する。

sessionを使うと便利。

app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
  # 記憶したURL (もしくはデフォルト値) にリダイレクト
  def redirect_back_or(default)
    redirect_to(session[:forwarding_url] || default)
    session.delete(:forwarding_url)
  end

  # アクセスしようとしたURLを覚えておく
  def store_location
    session[:forwarding_url] = request.original_url if request.get?
  end
end

request.original_urlについて見ていこう。

requestはもともとある特殊な変数である。
original_urlメソッドを渡してあげると、userがもともと行きたかったurlを参照することができる。

if request.get?について見ていこう。

本来ログインせずにpatchリクエストをしてupdateアクションを呼び出すのは意味がない。
なので、if request.get?でgetリクエストだった時だけ、session[:forwarding_url]に情報を格納するよーって話。

もともと行きたかったurlが発生するのは必ずログイン前のことなので、
store_locationメソッドをbefore_actionで指定したメソッドの中に記載する。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def edit
  end
  .
  .
  .
  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end

    # beforeアクション

    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

    # 正しいユーザーかどうか確認
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless current_user?(@user)
    end
end

もしログインしてなかったら、store_locationメソッドが呼び出されて、もともと行きたかったurlが参照できる。

そして、redirect_back_orメソッドをsessionコントローラのcreateメソッドの中に入れる。

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  .
  .
  .
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_back_or user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end
  .
  .
  .
end

本当はここでテスト通るはずなんだが、まさかの失敗。

NameError:         NameError: undefined local variable or method ` store_location' for #<UsersController:0x0000000006837bc8>
        Did you mean?  store_location
            app/controllers/users_controller.rb:51:in `logged_in_user'
            test/integration/users_edit_test.rb:22:in `block in <class:UsersEditTest>'

このようにエラーが出た。

で、これは全角スペースがcontrollerの方に含まれていたからなんだね。

全角スペースもrubyではメソッド名に入る。
なのでこれを取り除くとテストが通った。

全てのユーザーを表示する

まずはログインしてないユーザーがindexアクションをリクエストしたときにログインページに飛ぶかをテストする。

test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user       = users(:michael)
    @other_user = users(:archer)
  end

  test "should redirect index when not logged in" do
    get users_path
    assert_redirected_to login_url
  end
  .
  .
  .
end

この状態だとテストは失敗してしまう。
テストを通るようにするのは簡単。

users_controllerにindexアクションを定義して、before_actionにindexアクションを追加してあげればいいだけ。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  before_action :correct_user,   only: [:edit, :update]

  def index
  end

  def show
    @user = User.find(params[:id])
  end
  .
  .
  .
end

次にindexアクションに対応するviewを作っていこう。

app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 50 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>

ただ、これだと、gravatar_forメソッドに引数が2つ渡されている。
users_helperに定義したメソッドは引数が1つしか渡せない。

これを直す必要がある。

app/helpers/users_helper.rb
module UsersHelper

  # 渡されたユーザーのGravatar画像を返す
  def gravatar_for(user, options = { size: 80 })
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    size = options[:size]
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

これでエラーはなくなるが、cssが整ってないので、

app/assets/stylesheets/custom.scss
/* Users index */

.users {
  list-style: none;
  margin: 0;
  li {
    overflow: auto;
    padding: 10px 0;
    border-bottom: 1px solid $gray-lighter;
  }
}

それが終わったらheaderにリンクを設定する。

サンプルユーザーの作成

ページネーションを試したいが、30人手動で作成するのはかなり面倒。
なのでコンピュータに作ってもらおう。

まずはGemfileにFaker gemを追加します

source 'https://rubygems.org'

gem 'rails',          '5.1.6'
gem 'bcrypt',         '3.1.12'
gem 'faker',          '1.7.3'

bundleをする

データベース上にサンプルユーザーを生成するRailsタスク

次にサンプルユーザーの生成するためのコードを書く。

db/seeds.rb
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar")

99.times do |n|
  name  = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name:  name,
               email: email,
               password:              password,
               password_confirmation: password)
end

注目すべきはUser.create!

db/seedsはべきとうせいではないので、一度実行して、2度目を実行しようとすると、再度100人サンプルユーザーが作られてしまう。

ただ、メールアドレスはユニークネスのバリデーションを設定しているので、100回間違えることになる。

これを避けるために、!を追加して、1人目の時点でバリデーションに引っかかったら例外を出すようにしている。

faker gemの機能を試してみよう

$ rails db:migrate:reset
このコマンドを実行すると、開発用のDBのデータをリセットすることができる。

そして、
$ rails db:seed
このコマンドを実行すると、サンプルユーザーが100人作られる。

ページネーション

サンプルユーザーが100人できたので、ページネーション機能を作っていこう

source 'https://rubygems.org'

gem 'rails',                   '5.1.6'
gem 'bcrypt',                  '3.1.12'
gem 'faker',                   '1.7.3'
gem 'will_paginate',           '3.1.6'
gem 'bootstrap-will_paginate', '1.0.0'

will_pagenateというgemをインストールする。

gem 'bootstrap-will_paginate', '1.0.0'はページネーション機能とbootstrapを関連つける

pagenationを表示させる

pagenationを表示させるのは簡単で、

app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 50 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>

<%= will_paginate %>

pagenationを表示させたいところで<%= will_paginate %>と書くだけで良い。

ここで少し注意点

<%= will_paginate %>は本来<%= will_paginate @users %>となる。

今いるコンテキストの扱っているリソースを推測してくれて、pagenateする機能なのである。

これだけでは、ダメらしい。

具体的には、indexアクション内のallをpaginateメソッドに置き換えます (リスト 10.46)。ここで:pageパラメーターにはparams[:page]が使われていますが、これはwill_paginateによって自動的に生成されます。

app/controllers/users_controller.rbclass
UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  .
  .
  .
  def index
    @users = User.paginate(page: params[:page])
  end
  .
  .
  .
end

実装はこれで終わり。

pagenationテストを行う

pagenationのテストを行うにはテスト環境にユーザーが30人以上いないとダメ。

それを解決するには、

test/fixtures/users.yml
michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>

archer:
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>

lana:
  name: Lana Kane
  email: hands@example.gov
  password_digest: <%= User.digest('password') %>

malory:
  name: Malory Archer
  email: boss@example.gov
  password_digest: <%= User.digest('password') %>

<% 30.times do |n| %>
user_<%= n %>:
  name:  <%= "User #{n}" %>
  email: <%= "user-#{n}@example.com" %>
  password_digest: <%= User.digest('password') %>
<% end %>

というようにしてあげる。

統合テストのファイルを作成しよう

$ rails generate integration_test users_index

test/integration/users_index_test.rb
require 'test_helper'

class UsersIndexTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "index including pagination" do
    log_in_as(@user)
    get users_path
    assert_template 'users/index'
    assert_select 'div.pagination'
    User.paginate(page: 1).each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
    end
  end
end

今回のテストでは、paginationクラスを持ったdivタグをチェックして、最初のページにユーザーがいることを確認します。

30人以上のユーザーがいるとpaginationクラスを持つようになるらしい。

indexアクションのviewをリファクタリングしよう

app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <%= render user %>
  <% end %>
</ul>

<%= will_paginate %>

ここでは、renderをパーシャル (ファイル名の文字列) に対してではなく、Userクラスのuser変数に対して実行している点に注目してください。この場合、Railsは自動的に_user.html.erbという名前のパーシャルを探しにいくので、このパーシャルを作成する必要があります

app/views/users/_user.html.erb
<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
</li>

これはさらにリファクタリングができる。

app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <%= render @users %>
</ul>

<%= will_paginate %>

<%= render @users %>をすると、railsが自動的に

<% @users.each do |user| %>
    <%= render user %>
 <% end %>

のコードを展開してくれる。

なので、これを分かっていれば、
パーシャルを先に用意して、あとはrender @usersとすれば簡単にユーザーの情報を並べて表示することができる。

この状態でテストも通る。

このリファクタリングはかなりショートカットできるので覚えるように!!!

ユーザーを削除する

管理権限を持っていればユーザーを削除できるようにする。

まずは、

特権を持つ管理ユーザーを識別するために、論理値をとるadmin属性をUserモデルに追加します。この後で説明しますが、こうすると自動的にadmin?メソッド (論理値を返す) も使えるようになりますので、これを使って管理ユーザーの状態をテストできます。

$ rails generate migration add_admin_to_users admin:boolean

今回は作成されたマイグレーションファイルにデフォルト値を設定する

db/migrate/[timestamp]_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :admin, :boolean, default: false
  end
end

booleanなのでtrueかfalseかしか入らないと思いがちだが、どうやらnilも入るらしい。
なので、あらかじめdefaultオプションでfalseを指定しておく。

adminユーザーを作ってみよう

db/seeds.rb
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar",
             admin: true)

99.times do |n|
  name  = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name:  name,
               email: email,
               password:              password,
               password_confirmation: password)
end

そして、データベースをリセットして再度サンプルデータをページネーションで表示してあげる。

$ rails db:migrate:reset
$ rails db:seed

destroyアクションを実装しよう

まずはadminユーザーだけリンクが見えるようにしよう

app/views/users/_user.html.erb
<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
  <% if current_user.admin? && !current_user?(user) %>
    | <%= link_to "delete", user, method: :delete,
                                  data: { confirm: "You sure?" } %>
  <% end %>
</li>

userはuser_path(user)の略。

destroyアクションを作る

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def destroy
    User.find(params[:id]).destroy
    flash[:success] = "User deleted"
    redirect_to users_url
  end

  private
  .
  .
  .
end

これだけではダメ、
ログインしていて、かつadminユーザーじゃないとだめ

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]
  before_action :admin_user,     only: :destroy
  .
  .
  .
  private
    .
    .
    .
    # 管理者かどうか確認
    def admin_user
      redirect_to(root_url) unless current_user.admin?
    end
end

ユーザーの削除のテスト

まずはテスト用のサンプルユーザの中にadminユーザーを作る。

test/fixtures/users.yml
michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>
  admin: true

テストコードを書く。
以下は、adminユーザーだけどログインしてない、そして、ログインしてるけどadminユーザーじゃない場合はどちらもdestroyアクション使えないよねーってテスト

test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user       = users(:michael)
    @other_user = users(:archer)
  end
  .
  .
  .
  test "should redirect destroy when not logged in" do
    assert_no_difference 'User.count' do
      delete user_path(@user)
    end
    assert_redirected_to login_url
  end

  test "should redirect destroy when logged in as a non-admin" do
    log_in_as(@other_user)
    assert_no_difference 'User.count' do
      delete user_path(@user)
    end
    assert_redirected_to root_url
  end
end

統合テストも書いてみる。

test/integration/users_index_test.rb
require 'test_helper'

class UsersIndexTest < ActionDispatch::IntegrationTest

  def setup
    @admin     = users(:michael)
    @non_admin = users(:archer)
  end

  test "index as admin including pagination and delete links" do
    log_in_as(@admin)
    get users_path
    assert_template 'users/index'
    assert_select 'div.pagination'
    first_page_of_users = User.paginate(page: 1)
    first_page_of_users.each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
      unless user == @admin
        assert_select 'a[href=?]', user_path(user), text: 'delete'
      end
    end
    assert_difference 'User.count', -1 do
      delete user_path(@non_admin)
    end
  end

  test "index as non-admin" do
    log_in_as(@non_admin)
    get users_path
    assert_select 'a', text: 'delete', count: 0
  end
end
unless user == @admin
   assert_select 'a[href=?]', user_path(user), text: 'delete'
end

これは、@adminとuserが異なる場合、deleteのリンクが見えるよね?というテスト
逆に@admin = userの場合、deleteリンク見えませんよねーってこと

assert_difference 'User.count', -1 do
  delete user_path(@non_admin)
end

また、do endでUserの数が1少なくなることを表す時は-1を指定していることにも注目。

herokuのDBを初期化する

$ heroku pg:reset DATABASE

$ heroku run rails db:migrate
このコマンドって、pushしてからじゃないと、マイグレーションファイルがそもそもないので意味がないってことなんじゃない?

桜を作る
$ heroku run rails db:seed
$ heroku restart

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

PytorchをRubyで動かすtorch-rbをやってみた

はじめに

Rubyからpytorchを動かすtorch-rbを動かすところまでやってみたのでレポートします。
PyTorchRuby

torch-rbとは

image.png

torch-rbはAndrew Kane氏が作成・公開している一連のRuby向け機械学習ライブラリの再新作です。ここ数ヶ月、Ankane氏はonnxruntimeのバインディング、Tensorflowのバインディング、LightGBMやxgboostのバインディングと、ものすごい勢いでRuby向けの機械学習ライブラリを制作・公開してきました。なかでも最新作にあたるtorch-rbに関してはREADMEから強い気合が伝わってきます。これまでのAnkaneプロダクツは、いずれもruby-ffiを利用したものですが、今回はriceを利用しているという特徴もあります。

torch-rbは絶賛開発中なので、ここに書いた情報はすぐに古くなる可能性があります。なるべく最新の情報を参照してください。

PyTorchのインストール

まずはpytorchをインストールします。Macを使っている方は、brewコマンドで大丈夫みたいですが、Linuxを使っているのでサイトからダウンロードします。LanguageでC++を選択するのがポイントです。
たまたま手元のPCがノートパソコンでGPUの性能は高くないので、今回はCPUオンリーのものをダウンロードしました。

https://pytorch.org/
image.png

ダウンロードしたら解凍してできたlibtorchディレクトリをどこに配置したらいいのか、実はあまりよくわかっていませんが、とりあえず/usr/local/libあたりに配置しましてみました。

つぎに、Githubからtorch-rbのリポジトリーをcloneします。

git clone https://github.com/ankane/torch-rb

まずはbundle installしようと試みますが

bundle install

最初からエラーメッセージが出てつまづきます。どうやら、riceのインストールでエラーが発生しているようです。riceは Ruby Interface for C++ Extensions です。

Unfortunately Rice does not build against a staticly linked Ruby.
You'll need to rebuild Ruby with --enable-shared to use this library.

If you're on rvm:   rvm reinstall [version] -- --enable-shared
If you're on rbenv: CONFIGURE_OPTS="--enable-shared" rbenv install [version]

エラーメッセージを見ると--enable--shared オプションをつけてRubyをビルドし直すように表示されますので言われたとおりにします。

CONFIGURE_OPTS="--enable-shared" rbenv install 2.6.5

うまくインストールされました。

Installed ruby-2.6.5 to /home/kojix2/.rbenv/versions/2.6.5

気を取り直して、bundle installしなおします。今度はうまくいきました。
あとは以下のように--with-torch-dir オプションをつけてインストールします。(Macの場合はこれらの過程は不要のようですので詳しくはREADMEをご覧ください)

bundle exec rake build
gem install pkg/torch-rb-0.1.4.gem -- --with-torch-dir=/usr/local/lib/libtorch/

mnist example を試す

さて、インストールがうまくいったら、お決まりのmnist exampleを試してみます。

cd examples/mnist
wget https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
gem install npy
ruby main.rb

https://github.com/ankane/torch-rb/tree/master/examples/mnist

class Net < Torch::NN::Module
  def initialize
    super
    @conv1 = Torch::NN::Conv2d.new(1, 32, 3, stride: 1)
    @conv2 = Torch::NN::Conv2d.new(32, 64, 3, stride: 1)
    @dropout1 = Torch::NN::Dropout2d.new(p: 0.25)
    @dropout2 = Torch::NN::Dropout2d.new(p: 0.5)
    @fc1 = Torch::NN::Linear.new(9216, 128)
    @fc2 = Torch::NN::Linear.new(128, 10)
  end

  def forward(x)
    x = @conv1.call(x)
    x = Torch::NN::F.relu(x)
    x = @conv2.call(x)
    x = Torch::NN::F.max_pool2d(x, 2)
    x = @dropout1.call(x)
    x = Torch.flatten(x, start_dim: 1)
    x = @fc1.call(x)
    x = Torch::NN::F.relu(x)
    x = @dropout2.call(x)
    x = @fc2.call(x)
    output = Torch::NN::F.log_softmax(x)
    output
  end
end

out.gif

左のターミナルにhtopによるリソースの使用状況、右のターミナルに学習の進捗が表示されています。CPUを4コア使って頑張って計算してくれているのがわかりますね。しかしCPUやメモリをカツカツに使っている感じではなくて、バランスが取れていていいですね。

最終結果はこんな感じでした。

Train Epoch: 14 [59904/60000 (100%)] Loss: 0.197016
Test set: Average loss: 0.0298, Accuracy: 9909/10000 (99%)

素晴らしい。
推論だけでなく学習もPythonと遜色のない速度で実行できるRubyのディープラーニングライブラリは、私が知ってる範囲ではこれがはじめてです。今回はノートPCを使用して、i5-7200U CPUで計算しましたがまあまあ短時間で学習を終えることができました。

real    18m39.163s
user    47m20.107s
sys 0m26.818s

(ブラウザとか開いていたのでさらに早くなるかも)

ちょうどPFNがChainerをやめてPytorchに行くというニュースが入ってきました。今はちょうどそんな時期ですが、やっとRubyでもディープラーニングを勉強できる環境が整備されつつあるのではないでしょうか。

この記事は以上です。

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

PyTorchをRubyで動かすtorch-rbをやってみた

はじめに

RubyからPyTorchを動かすTorch-rbを動かすところまでやってみたのでレポートします。
PyTorchRuby

Torch-rbとは

image.png

Torch-rbはAndrew Kane氏が作成・公開している一連のRuby向け機械学習ライブラリの最新作です。ここ数ヶ月、Ankane氏はonnxruntimeのバインディング、Tensorflowのバインディング、LightGBMやxgboostのバインディングと、ものすごい勢いでRuby向けの機械学習ライブラリを制作・公開してきました。なかでも最新作にあたるtorch-rbに関してはREADMEから強い気合が伝わってきます。これまでのAnkaneプロダクツは、いずれもruby-ffiを利用したものですが、今回はriceを利用しているという特徴もあります。

torch-rbは絶賛開発中なので、ここに書いた情報はすぐに古くなる可能性があります。なるべく最新の情報を参照してください。

PyTorchのインストール

まずはpytorchをインストールします。Macを使っている方は、brewコマンドで大丈夫みたいですが、Linuxを使っているのでサイトからダウンロードします。LanguageでC++を選択するのがポイントです。
たまたま手元のPCがノートパソコンでGPUの性能は高くないので、今回はCPUオンリーのものをダウンロードしました。

https://pytorch.org/
image.png

ダウンロードしたら解凍してできたlibtorchディレクトリをどこに配置したらいいのか、実はあまりよくわかっていませんが、とりあえず/usr/local/libあたりに配置しましてみました。

つぎに、Githubからtorch-rbのリポジトリーをcloneします。

git clone https://github.com/ankane/torch-rb

まずはbundle installしようと試みますが

bundle install

最初からエラーメッセージが出てつまづきます。どうやら、riceのインストールでエラーが発生しているようです。rice は Ruby Interface for C++ Extensions です。

Unfortunately Rice does not build against a staticly linked Ruby.
You'll need to rebuild Ruby with --enable-shared to use this library.

If you're on rvm:   rvm reinstall [version] -- --enable-shared
If you're on rbenv: CONFIGURE_OPTS="--enable-shared" rbenv install [version]

エラーメッセージを見ると--enable--shared オプションをつけてRubyをビルドし直すように表示されますので言われたとおりにします。

CONFIGURE_OPTS="--enable-shared" rbenv install 2.6.5

うまくインストールされました。

Installed ruby-2.6.5 to /home/kojix2/.rbenv/versions/2.6.5

気を取り直して、bundle installしなおします。今度はうまくいきました。
あとは以下のように--with-torch-dir オプションをつけてインストールします。(Macの場合はこれらの過程は不要のようですので詳しくはREADMEをご覧ください)

bundle exec rake build
gem install pkg/torch-rb-0.1.4.gem -- --with-torch-dir=/usr/local/lib/libtorch/

mnist example を試す

さて、インストールがうまくいったら、お決まりのmnist exampleを試してみます。

cd examples/mnist
wget https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
gem install npy
ruby main.rb

https://github.com/ankane/torch-rb/tree/master/examples/mnist

class Net < Torch::NN::Module
  def initialize
    super
    @conv1 = Torch::NN::Conv2d.new(1, 32, 3, stride: 1)
    @conv2 = Torch::NN::Conv2d.new(32, 64, 3, stride: 1)
    @dropout1 = Torch::NN::Dropout2d.new(p: 0.25)
    @dropout2 = Torch::NN::Dropout2d.new(p: 0.5)
    @fc1 = Torch::NN::Linear.new(9216, 128)
    @fc2 = Torch::NN::Linear.new(128, 10)
  end

  def forward(x)
    x = @conv1.call(x)
    x = Torch::NN::F.relu(x)
    x = @conv2.call(x)
    x = Torch::NN::F.max_pool2d(x, 2)
    x = @dropout1.call(x)
    x = Torch.flatten(x, start_dim: 1)
    x = @fc1.call(x)
    x = Torch::NN::F.relu(x)
    x = @dropout2.call(x)
    x = @fc2.call(x)
    output = Torch::NN::F.log_softmax(x)
    output
  end
end

out.gif

左のターミナルにhtopによるリソースの使用状況、右のターミナルに学習の進捗が表示されています。CPUを4コア使って頑張って計算してくれているのがわかりますね。しかしCPUやメモリをカツカツに使っている感じではなくて、バランスが取れていていいですね。

最終結果はこんな感じでした。

Train Epoch: 14 [59904/60000 (100%)] Loss: 0.197016
Test set: Average loss: 0.0298, Accuracy: 9909/10000 (99%)

素晴らしい。
推論だけでなく学習もPythonと遜色のない速度で実行できるRubyのディープラーニングライブラリは、私が知ってる範囲ではこれがはじめてです。今回はノートPCを使用して、i5-7200U CPUで計算しましたがまあまあ短時間で学習を終えることができました。

real    18m39.163s
user    47m20.107s
sys 0m26.818s

(ブラウザとか開いていたのでさらに早くなるかも)

ちょうどPFNがChainerをやめてPytorchに行くというニュースが入ってきました。今はちょうどそんな時期ですが、やっとRubyでもディープラーニングを勉強できる環境が整備されつつあるのではないでしょうか。

この記事は以上です。

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

プログラミングを始めて1年

Ubiregi Advent Calendar 2019 6日目です!

なにこれ

今日は技術の話でもなく、そもそも開発のお仕事に(まだ)就いていない人が1年、プログラミング(RubyとちょっとRails)を楽しく続けることが出来ているのでそれに関する話を書きます。

自己紹介

株式会社ユビレジの原と申します。カスタマーサクセス(CS)部に所属しております。仕事はお客さんとの調整事やらなんやらで仕事では全くプログラミングしていません。

そもそもなんでプログラミングやり始めたのか

純粋に楽しそうだったからです。結構もういい歳ですが、本気でジョブチェンジをしたい。世間体のあり様で言えば、管理職にでもなって部下を指導するみたいな役割をこなさないといけないんじゃないかな〜とたまに思うこともあります。ですがそれが自分にとって本当にプラスになる生き方なのかと考えると不必要なストレスを抱えて生きるよりは、楽しく仕事できた方が良いかなーとも思います。

いろんな言語がある中でなぜRuby?

単純にユビレジがRuby on Railsで動いている & 不明点あれば弊社開発チームに質問できる & 学んでいくうちにわかったことですが、直感的でわかりやすい言語だから あたりでしょうか。

プログラミングは難しいとよく言われているけど実際どうだったか

プログラミングに限らないと思いますが、誰でも最初は難しいと感じるのではないでしょうか。
配列とかハッシュとか横文字がズラズラ出てきて、なんなんこれ?と。役立つんかいなと。
引数とか戻り値とかようわからん!と嘆いていました・・・。
わからん→ググる→理解する(言っていることがわからなければ社内で質問する)→1ヶ月ぐらいすると忘れている・・・の繰り返しでした。
噛めば噛むほど味が出るという訳ではないですが、時間を掛けて学んでいけばゆくゆくはできるようになるんじゃないかなと思います。
あと「プログラミング 勉強法」なんかでググると3ヶ月でプログラマーデビュー!みたいな広告を見ますが、私のようにまっさらな状態からのスタートだと厳しいんじゃないかなと思います。なんらかの言語を学んでいるとかならいけるのかな・・・。毎日8時間程度の学習時間を確保できるのであれば(羨望)
あとは楽しく学べているのか、習慣化出来ているのかぐらいですかね?

今までやってきたこと

【2018年 12月〜2019年 1月】

・Rubyを学び始める。使った参考書はゼロからわかる Ruby 超入門を使って基礎の基礎から始めました。入門書に関しては他にも色々あると思いますが、プログラミング自体を初めて学ぶ方にはオススメ!(コードエディタの説明とか役に立ちました)

【2019年 2月〜6月】

・アプリ作成できることが最終目標なのだから、Railsチュートリアルをやろうと奮い立ちました。しかし今思うとやや時期尚早だったなと。中身は2割ぐらいしか理解できず。とりあえず写経して動くものが作れるという体験をしました。
・チュートリアルの合間にプロを目指す人のためのRuby入門も並行して取り組みました。こちらは超入門と比較するとかなりボリュームがありますが、中身は親切丁寧に書かれているので、理解しながら読んでいけば成果は出るかと。ただし1度読んで写経したからと言ってすぐに使えるものでもないと思うので、忘れた頃に読み返すと良いかなーと思います。

【2019年 7月~9月】

・検索すると兎にも角にもアプリを作るのがプログラミングを理解するのに一番の近道と言われているのでアプリ作成かなとは思うものの、チュートリアルの理解が微妙だし、もう一度Railsチュートリアルをログイン機構実装まで復習と写経を行いました。今度は中身を理解できるまで熟読と写経を行う。またすこーしだけ理解が進んだ。

【2019年 10月〜11月】

・さあもういけるやろと意気込んでみたものの、やはりよくわからない・・・。
悩んだ時に見返していたこの記事を見ると「チュートリアルやったら次は現場Rails見るといいんでないかい」とあったので素直に見ることにしました。これからですが。
※ちなみにこの記事に書いてある事は結構信憑性が高いと個人的に思っています。
・そして10月からHackerRankなるものを始めました。開発チームの方から良かったらどう?みたいな感じで。最初はどうかなーと思っていましたが、問題を解く日々が続いております。Rubyの復習にもなるし、算数やアルゴリズムの勉強にもなるので一石二鳥!

この一年で購入したものや読んだ本

・MacBook Air 2016(中古)
本当はProの方がいいんですけど、プログラミングが続くかどうかわからなかったので日和ってしまった & 奥さんにめちゃ懇願してどうにか許しを得ての購入だった。でも軽いし持ち運び便利だから最初はAirでもいいんじゃなかろうか。確か7諭吉ぐらい。

ゼロからわかる Ruby 超入門
プロを目指す人のためのRuby入門
この2冊でRubyの大まかな部分を学習
プログラマーの数学
ちょっと難しい箇所もありましたが、全体的にわかりやすく書かれていました。込み入った問題にどう取り組めば良いのかなどの考え方が参考になる
Webを支える技術
HTTPとかRESTなどを知るため。この世界では鉄板の様です。
ベタープログラマー ―優れたプログラマになるための38の考え方とテクニック
会社でほぼ毎日お昼の時間に15分程度の読書会を行っており、そこで読みました。こういうプログラマーになりましょう!的な内容の本です。

まあまあ読んでいましたね?いや少ないか・・・。まあ読めばいいってものじゃないし!

今後の展望

1分でも早く仕事でプログラミングをやりたい!とは常々思っていますが、じゃあお前これやれやって言われてガッテン承知!とはいかないものです?手っ取り早く認めてもらうには簡単でも良いから何らかのアプリが作成できてからですかね・・・。1年も経ったわけだし何か成果物欲しいところ(自分のモチベーションを上げるにも)。

最後に

まだまだ頑張ります!

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

Rails 6.0の "Active Storage's ImageProcessing transformer doesn't support :combine_options" という警告に対処する方法

困っていたこと

Rails 5.2時代にActiveStorageでこんなコードを書いていました。

user.avatar.variant(
  combine_options: { 
    resize: "150^", 
    gravity: "Center", 
    crop: "150x150+0+0", 
    auto_orient: true  
  }
)

やっていることは縦長だったり横長だったりする画像を、中心で正方形に切り抜いて150x150サイズに縮小することです。(mini_magickを使用)

つまり、この画像↓を、
3500x1500-1.png

こうするわけです↓
3500x1500.png

このコードをRails 6.0に上げるとこんな警告が出ました。

DEPRECATION WARNING: Generating image variants will require the image_processing gem in Rails 6.1. Please add `gem 'image_processing', '~> 1.2'` to your Gemfile. 

なにやらimage_processing gemをインストールしろ、と言われているので、言われたとおりGemfileに追加します。

Gemfile
gem 'image_processing', '~> 1.2'
bundle install

すると、今度は警告の内容が次のように変わりました。

DEPRECATION WARNING: Active Storage's ImageProcessing transformer doesn't support :combine_options, as it always generates a single ImageMagick command. Passing :combine_options will not be supported in Rails 6.1.

こちらの警告は:combine_optionsはRails 6.1でサポートされなくなるぜ!と言われてるものの、具体的な修正方法がわかりません。

解決方法

この警告が導入されたコミットを確認した結果、どうやら:combine_optionsをなくしてこう書けば良いっぽいです。

user.avatar.variant( 
  resize: "150^", 
  gravity: "Center", 
  crop: "150x150+0+0", 
  auto_orient: true  
)

これで警告が消え、画像の切り抜きもうまくいきました。

ただ、これがベストプラクティスかどうかは確信がないので、もっと良い解決策を知ってる人がいたらコメントお待ちしています!

追記:こんなやり方があった!

同僚の @aki77 が「こんなやり方がありますよー」と、もっと便利な書き方を教えてくれました。(どうもありがとう!)

確かもうちょっと簡単に書けるようになったはずと思って調べてみました。
多分↓で良さそう。

user.avatar.variant(resize_to_fill: [150, 150])
https://edgeguides.rubyonrails.org/active_storage_overview.html#transforming-images

carrierwaveでも同じだったなーと思ったら、あっちもimage_processing使ってるんですね。
https://github.com/carrierwaveuploader/carrierwave/blob/master/carrierwave.gemspec#L27

ということで、Rails 6ではこれだけでOKです!
auto_orient: trueもデフォルトでセットされます)

user.avatar.variant(resize_to_fill: [150, 150])

めっちゃシンプルになりましたね!やったー?

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

rails cが動かねぇ!

rails c が息をしていない…

別件でrailsのバージョンアップをして、railsを入れ直した後のこと。
consoleで確認したいことがあり、rails cを叩く。
…反応がない、ただの屍のようだ。
いや困るねん。やめてクレメンス。

いらないプロセスは躊躇なく殺そうね

てことでプロセスを確認。

ps aux | grep rails

…何もないな。
調べたところ、rails cではspringというプロセスも走るとのこと。

ps aux | grep spring
~~ 8324   0.0  0.0  4352360   3228 s009  S+   火03PM   0:00.76 spring server | ~~ | started 47 hours ago

いやがった!!お前のせいか!!

kill -9 8324

そしてめでたくconsoleが起動。
いやぁ良かった。
無駄なプロセスは殺そう。

追記

spring stop

でも可。
これだとプロセス確認せずに止められるから便利ね。
ありがとう、友よ。

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

【自分メモ】ProgateでRubyを学ぶ【ウルトラ初心者】

はじめに

Progateを使ってRubyを学習しています。
自分なりのメモです。
相当あたまがわるいので文章力は底辺です。

変数

シングルクォーテーションで囲むと#{変数}は展開されない。
ダブルクォーテーションで囲む必要がある。
いかなるときも、ダブルで囲んでおけば問題ないですね。
むしろシングルを使う意味ってどこかにあるんでしょうか。

sample.rb
name = "田中"
puts "こんにちは#{name}さん" #こんにちは田中さん
puts 'こんにちは#{name}さん' #こんにちは#{name}さん

数字と文字列を連結することはできない。これは変数でも同じ。
文字列の中に数値を埋め込む場合には #{変数} を利用する。

sample2.rb
age = 25
puts "私は" + age + "歳です" #エラー
puts "私は#{age}歳です" #私は25歳です

ハッシュ

複数の値を管理する方法のひとつ。
- 配列は要素を順番に並べて管理する
- ハッシュはキーと値でセットにして管理する

hush.rb
person = {age: 25, height: 158, name: "田中"}
puts  "私は#{person[:age]}歳です"  #私は25歳です

さいごに

Javaのように変数の型を決める必要もないし楽チンだな〜という印象。
逆に、Javaちゃんとやれば他の言語の習得簡単だろなあと。

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

ActiveRecordを使いつつDBのデータをメモ化して扱ってみる

はじめに

一度登録されたら、その後あまり更新されないマスター系のデータってありますよね。
普通にDBのテーブルを作成してActiveRecordを使ったり、moduleやclassにデータをハードコーディングして扱う場合もあると思います。

テーブルが不要な場合は ActiveHash というgemを使うと便利で高速ですが、なんらかの理由でDBを使いたい場合もあるかもしれません。
(例えば、Railsから切り離されたレポートシステムなどから同じマスターデータを参照したい場合など)
ということで、ActiveRecord を利用しつつ普段はオンメモリで高速にデータを扱う簡単なライブラリを作成してみました。

実装したもの要約

  • マスター系データを扱うModelクラスで、全レコードのオブジェクトをメモ化するメソッド
  • メモ化したデータを対象にfind, selectを行うメソッド
  • 上記Modelに対しbelongs_toの関係にあるModelから参照する場合も、メモ化されたオブジェクトを利用するAssociation

使い方

インストール

RubyGemsに登録したのでGemfileを使ったりgemコマンドを使ってインストールできます。
https://rubygems.org/gems/ar_memoization

Gemfile
gem 'ar_memoization'

DBスキーマの例

サンプルとして、以下のように都道府県を扱うprefecturesと、お店情報を扱うshopsテーブルを定義しました。
shopsprefecturesに対する外部キーを持っており、prefecturesはあまり更新されないものとします。

db/create_tables.rb
class CreateAllTables < ActiveRecord::Migration[5.0]
  def self.up
    create_table(:prefectures) do |t|
      t.string :name
    end

    create_table(:shops) do |t|
      t.belongs_to :prefecture
      t.string :name
    end
  end
end

マスターデータを定義するModelクラス

全レコードのインスタンスをメモ化したいModelクラス内でArMemoization::PrimaryMethods モジュールをextendします。

app/models/prefecture.rb
#
# 都道府県を扱うModel
#
class Prefecture < ApplicationRecord
  extend ArMemoization::PrimaryMethods
end

追加されるメソッド

  • .find_memo(id)
    • メモ化したインスタンスから、引数のIDに一致するデータを返す
    • IDをキーにしたHashから該当レコードのオブジェクトを返すので爆速
  • .detect_memo(&block)
    • メモ化したレコードのオブジェクトをブロック引数で渡し、ブロックがtrueを返したデータを1件返す
  • select_memos(&block)
    • メモ化したレコードのオブジェクトをブロック引数で渡し、ブロックがtrueを返したデータを配列で返す
  • all_memos
    • メモ化したインスタンスの配列を全て返す
  • reload_memos
    • メモ化したオブジェクトをDBからリロードする

使い方の例

事前準備(データ投入)
[[1, "東京"], [2, "大阪"], [3, "名古屋"]].each do |ident, name|
  Prefecture.create!(id: ident, name: name)
end
# ID:2 のレコードを取得
prefecture = Prefecture.find_memo(2)

# インスタンスメソッド kanto_area? がtrueを返すオブジェクトを取得
prefecture = Prefecture.detect_memo{|pref| pref.kanto_area? }
prefecture = Prefecture.detect_memo(&:kanto_area?) # syntax sugar

# インスタンスメソッド kanto_area? がtrueを返すオブジェクトの配列を取得
prefectures = Prefecture.select_memos(&:kanto_area?)

実装の詳細はこちら

マスターデータへの外部キーを持つModelクラス

マスターデータのModelに対してbelongs_toの関係を持つModelクラスにはArMemoization::ForeignMethodsをextendします。
また、belongs_toの代わりにbelongs_to_memoizedを使いAssociationを定義します。

app/models/shop.rb
class Shop < ApplicationRecord
  extend ArMemoization::ForeignMethods
  belongs_to_memoized :prefecture
end

(通常ないと思いますが)belongs_toと同時に利用することも出来ます。

app/models/shop.rb
  belongs_to :prefecture
  belongs_to_memoized :memoized_prefecture, class_name: "Prefecture", foreign_key: "prefecture_id"

belongs_to_memoized は内部でbelongs_toを実行した後で、関連名でもあるreaderメソッド(上記例ではShop#prefecture)をoverrideし、レコードをDBからロードする処理の代わりにメモ化済みのオブジェクトをAssociationとしてセットしています。

Association以外で追加されるメソッド

  • .where_memoized(association_name, method_name, &block)
    • 関連Modelのメモ化されたオブジェクトを用い、join + where 的な絞り込みを行う
    • ActiveRecord::Relation を返す

使い方の例

# Prefectureオブジェクトの中で、kanto_area?がtrueを返すオブジェクトのIDに関連しているShopのRelationを返す
# 第2引数として関連Modelのインスタンスメソッド名か、ブロックを渡す
Shop.where_memoized(:prefecture, :kanto_area?).limit(3)
Shop.where_memoized(:prefecture){|pref| pref.kanto_area? }.limit(3)
発行されるクエリ
SELECT  `shops`.* FROM `shops` WHERE `shops`.`prefecture_id` = 1 LIMIT 3

同一の条件文を実装するのに、scopeとインスタンスメソッドの両方でコードを書く必要がなくなるので、自分的にはちょっと良い感じです。

実装の詳細はこちら

今は出来ないけど出来そうなこと

  • ID以外のPrimary Key に対応
  • has_one, has_many からメモ化済みオブジェクトの取得
  • create, saveのコールバックでメモ化したオブジェクトを差し替える
  • 開発環境ではメモ化したオブジェクトを適切なタイミング(before_actionなど)で簡単にリロードする仕組み

まとめ

ほとんどの場合はActiveHashを使えば良いと思うので、このライブラリの使いどころがあまり思いつきません。とりあえず「こんなのあったらどうだろう?」な思いつきを形にしてみました。むしろ使い道を教えてください?

それでは、もっと有意義な時間を過ごすべくデザインパターンの勉強でもしてみましょう。
12日目は @nagata03 さんです!

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

普段使っているgemにプルリクを送ってマージされた

(この記事は、高専OBOG Advent Calendar 2019の12/5に送った記事です。)

要約

普段使っているgemに、Pull Requestを送って、マージされました。

LazyHighChartsというgemの動きが気に入らなくてモンキーパッチを当てて使っていました。
自分のアプリケーション以外でも役に立つ、gem本体に取り込まれるべき修正だと考えたので、gemのリポジトリにプルリクを送ることにしました。

流れはこんな感じです。

  1. 不具合を見つける
  2. 手元でgemの修正を試す
  3. githubのリポジトリに修正を送る
  4. マージされる

PRがマージされるまで

1. 不具合を見つける

LazyHighChartsは、Highcharts JSというチャートを描画するJavaScriptライブラリを、RailsやSinatraなどのフレームワーク内で簡単に使えるようにするgemです。
詳しい紹介は本家をご覧ください。https://github.com/michelson/lazy_high_charts

RailsでJavaScriptの読み込みにdefer属性を付与した場合、チャートが描画されない、という不具合に遭遇しました。
viewに書き出されたjsを見てみると、どうやらLazyHighChartsがこしらえたjsに問題があるようでした。

2. 手元でgemの修正を試す

手元でgemの修正をためします。

問題になっている箇所を書き出しているModuleを探して、モンキーパッチを当てます。
IntelliJ IDEAだとコードジャンプで該当箇所に飛べるので、すぐにModuleやClassの名前がわかって便利ですね!
モンキーパッチを当てると、問題なくチャートが描画されました。
(しばらくこの状態で本番で使っていました)

次はgemのリポジトリをforkして、gemに修正を加えます。
Gemfileのlazy_high_chartsの取得先をこんな感じにforkしたリポジトリ&ブランチに書き換えて、手元で修正版のgemを試します。

gem 'lazy_high_charts', git: 'git@github.com:kosappi/lazy_high_charts.git', branch: 'use_event_listener'

この修正版も、モンキーパッチのときと同じように、問題なくチャートを描画してくれました。

3. githubのリポジトリに修正を送る

手元で動作確認ができたので、本家にも同じ修正を送ります。
まずはREADME.mdを読んで、どのような流れで修正を提案するのか見てみます。README.md
Contributingを読んでみると、このように書いてあります。

  • Fork the project
  • Do your changes and commit them to your repository
  • Test your changes. We won't accept any untested contributions (except if they're not testable).
  • Create an issue with a link to your commits.

まずは動作確認して、それからissueを作って欲しい、とのことでした。
動作確認は済んでいるので、issueを作ります。
英語は得意ではないので、すでにマージされたPRや、closeされたissueを参考にしつつ、時間をかけて書くことになりました。
作ったissue

1日経つと、メンテナからPRを作って欲しいという旨の返事がありました。
PRを作ります。
作ったPR

PRを作ってしばらくするとCIがコケて赤くなっていました。
specを少し修正するだけで解決したんですが、事前にforkしたリポジトリでもCIを回しておくと、ベターかも知れません。

あとはマージされるのを待ちます。

4. マージされる

PRを出してから1日後、マージされました。
やったね!

感想

RubyやRailsを仕事で触りだして数年になりますが、めちゃくちゃgemを使うわりには、contributeする機会はまったくありませんでした。
タダで使ってばっかりでなんかスマン、という気持ちがありました。
今回、はじめてcontributeすることができて、とてもうれしいです。

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

LazyHighChartsにプルリクを送ってマージされた

(この記事は、高専OBOG Advent Calendar 2019の12/5に送った記事です。)

要約

普段使っているgemに、Pull Requestを送って、マージされました。

LazyHighChartsというgemの動きが気に入らなくてモンキーパッチを当てて使っていました。
自分のアプリケーション以外でも役に立つ、gem本体に取り込まれるべき修正だと考えたので、gemのリポジトリにプルリクを送ることにしました。

流れはこんな感じです。

  1. 不具合を見つける
  2. 手元でgemの修正を試す
  3. githubのリポジトリに修正を送る
  4. マージされる

PRがマージされるまで

1. 不具合を見つける

LazyHighChartsは、Highcharts JSというチャートを描画するJavaScriptライブラリを、RailsやSinatraなどのフレームワーク内で簡単に使えるようにするgemです。
詳しい紹介は本家をご覧ください。https://github.com/michelson/lazy_high_charts

RailsでJavaScriptの読み込みにdefer属性を付与した場合、チャートが描画されない、という不具合に遭遇しました。
viewに書き出されたjsを見てみると、どうやらLazyHighChartsがこしらえたjsに問題があるようでした。

2. 手元でgemの修正を試す

手元でgemの修正をためします。

問題になっている箇所を書き出しているModuleを探して、モンキーパッチを当てます。
IntelliJ IDEAだとコードジャンプで該当箇所に飛べるので、すぐにModuleやClassの名前がわかって便利ですね!
モンキーパッチを当てると、問題なくチャートが描画されました。
(しばらくこの状態で本番で使っていました)

次はgemのリポジトリをforkして、gemに修正を加えます。
Gemfileのlazy_high_chartsの取得先をこんな感じにforkしたリポジトリ&ブランチに書き換えて、手元で修正版のgemを試します。

gem 'lazy_high_charts', git: 'git@github.com:kosappi/lazy_high_charts.git', branch: 'use_event_listener'

この修正版も、モンキーパッチのときと同じように、問題なくチャートを描画してくれました。

3. githubのリポジトリに修正を送る

手元で動作確認ができたので、本家にも同じ修正を送ります。
まずはREADME.mdを読んで、どのような流れで修正を提案するのか見てみます。README.md
Contributingを読んでみると、このように書いてあります。

  • Fork the project
  • Do your changes and commit them to your repository
  • Test your changes. We won't accept any untested contributions (except if they're not testable).
  • Create an issue with a link to your commits.

まずは動作確認して、それからissueを作って欲しい、とのことでした。
動作確認は済んでいるので、issueを作ります。
英語は得意ではないので、すでにマージされたPRや、closeされたissueを参考にしつつ、時間をかけて書くことになりました。
作ったissue

1日経つと、メンテナからPRを作って欲しいという旨の返事がありました。
PRを作ります。
作ったPR

PRを作ってしばらくするとCIがコケて赤くなっていました。
specを少し修正するだけで解決したんですが、事前にforkしたリポジトリでもCIを回しておくと、ベターかも知れません。

あとはマージされるのを待ちます。

4. マージされる

PRを出してから1日後、マージされました。
やったね!

感想

RubyやRailsを仕事で触りだして数年になりますが、めちゃくちゃgemを使うわりには、contributeする機会はまったくありませんでした。
タダで使ってばっかりでなんかスマン、という気持ちがありました。
今回、はじめてcontributeすることができて、とてもうれしいです。

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

Rails6 のちょい足しな新機能を試す110(TranslationHelper#translate default オプション編)

はじめに

Rails 6 に追加された新機能を試す第110段。 今回は、TranslationHelper#translate default オプション編です。
Rails 6 では、 TranslationHelper#translatedefault オプションで指定された値が Hash の場合に、その Hash が返されるようになりました。

Ruby 2.6.5, Rails 6.0.1 で確認しました。(Rails 6.0.0 時点で修正されています。)

$ rails --version
Rails 6.0.1

今回は、適切な使い道を思いつけませんでした。
で、言語 (locale) 毎に数字をカンマ区切りで表示するだけの、Railsアプリケーションを作って試してみることにします。

Rails プロジェクトを作成する

$ rails new rails_sandbox
$ cd rails_sandbox

Controller と View を作る

今回は、数字を表示するための NumbersController と index ページを作成します。

$ bin/rails g controller Numbers index

Helper メソッドを作成する

locale_number_with_delimiter メソッドを作成します。
引数は、 数字と locale です。
default_number_format メソッドをコールして、locale に対するデフォルトフォーマットを決定します。
TranslationHelper#translate メソッド (t メソッド)を使って、 number.format を取得し、取得できなかった場合のために、default を指定します。
ここが、今回の確認ポイントになります。
number_with_delimiter メソッドで数字を変換します。

app/helpers/numbers_helper.rb
module NumbersHelper
  def locale_number_with_delimiter(number, locale)
    default = default_number_format(locale)

    format = t('number.format', locale: locale, default: default) # ここがちょい足し機能
    number_with_delimiter(number, format)
  end

  private

  def default_number_format(locale)
    case locale
    when :en, :ja
      { delimiter: ',', separator: '.' }
    when :de, :it
      { delimiter: '.', separator: ',' }
    when :sv
      { delimiter: ' ', separator: ',' }
    when :ruby
      { delimiter: '_', separator: '.' }
    else
      { delimiter: ',', separator: '.' }
    end
  end
end

Controller を修正する

NumbersControllerindex メソッドで、 @locales@number を設定します。

app/controllers/numbers_controller.rb
class NumbersController < ApplicationController
  def index
    @locales = %i[en ja de it sv ruby]
    @number = 10000.5
  end
end

View を修正する

View では、 Language(Locale) と数字を表形式で表示します。

app/views/numbers/index.html.erb
<h1>Numbers#index</h1>
<table>
  <thead>
    <tr>
      <th>
        Language
      </th>
      <th>
        Number
      </th>
    </tr>
  </thead>
  <tbody>
    <% @locales.each do |locale| %>
      <tr>
        <td><%= locale %></td>
        <td><%= locale_number_with_delimiter(@number, locale) %></td>
      </tr>
    <% end %>
  </tbody>
</table>

config/application.rb を修正する

動作確認目的なので、locale のエラーが出ないように、 config/application.rb に、 I18n.enforce_available_locales = false を追加します。

module App
  class Application < Rails::Application
    ...
    I18n.enforce_available_locales = false
  end
end

rails server を実行してブラウザで表示する

rails server を実行してブラウザで表示すると、それぞれの Language(locale) に合わせた数字の表示になっています。
110_rails6.png

Rails 5では

TranslationHelper#translate が default で指定した Hash を返さないため、NoMethodError になってしまい動作しません。
110_rails5.png

試したソース

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

参考情報

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

KibanaのAPIにrubyからアクセスする

概要

Kibanaに対してRubyからVisualizeやDashboardを作ったりしたかったので書いた

コード

class KibanaClient
  def self.call method, path, data
    endpoint = "http://localhost:5602#{path}"

    authorization = 'Basic ' + Base64.encode64("elastic:changeme").chomp
    headers = {
      authorization: authorization,
      "kbn-xsrf": "kibana",
      content_type: :json,
      accept: :json
    }

    response = nil
    begin
      response = RestClient.send(method.to_s, endpoint, data.to_json, headers)
    rescue => e
      puts e.response
      response = e.response
    end

    response
  end
end

Tips

  • kbn-xsrfが無いと弾かれる
  • Basic認証はKibanaのユーザで行う
  • リクエストに必要なJSONはKibanaのSaved ObjectsからInspectすると見れる
  • Saved ObjectsのExportで更に詳しく見れる

サンプル

class KibanaClient
  def self.call method, path, data
    endpoint = "http://localhost:5602#{path}"

    authorization = 'Basic ' + Base64.encode64("elastic:changeme").chomp
    headers = {
      authorization: authorization,
      "kbn-xsrf": "kibana",
      content_type: :json,
      accept: :json
    }

    response = nil
    begin
      response = RestClient.send(method.to_s, endpoint, data.to_json, headers)
    rescue => e
      puts e.response
      response = e.response
    end

    response
  end

  def self.gen_metric id, index_pattern, title, references = nil, query = nil
    references = [
      {
        "id": index_pattern,
        "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
        "type": "index-pattern"
      }
    ] if references.blank?
    query = {
      "query": {
        "query": "",
        "language": "kuery"
      }, 
      "filter": [], 
      "indexRefName": "kibanaSavedObjectMeta.searchSourceJSON.index"
    } if query.blank?

    state = {
      "title": title,
      "type": "metric",
      "params": {
        "metric": {
          "percentageMode": false,
          "useRanges": false,
          "colorSchema": "Green to Red",
          "metricColorMode": "None",
          "colorsRange": [
            {
              "type": "range",
              "from": 0,
              "to": 10000
            }
          ],
          "labels": {
            "show": false
          },
          "invertColors": false,
          "style": {
            "bgFill": "#000",
            "bgColor": false,
            "labelColor": false,
            "subText": "",
            "fontSize": 60
          }
        },
        "dimensions": {
          "metrics": [
            {
              "type": "vis_dimension",
              "accessor": 0,
              "format": {
                "id": "number",
                "params": {}
              }
            }
          ]
        },
        "addTooltip": true,
        "addLegend": false,
        "type": "metric"
      },
      "aggs": [
        {
          "id": "1",
          "enabled": true,
          "type": "count",
          "schema": "metric",
          "params": {}
        }
      ]
    }

    json = {
      "attributes": {
        "description": "",
        "kibanaSavedObjectMeta": {
          "searchSourceJSON": query.to_json
        },
        "title": title,
        "uiStateJSON": "{}",
        "visState": state.to_json
      },
      "references": references
    }

    api_path = "/api/saved_objects/visualization/#{id}?overwrite=true"

    response = KibanaClient.call(:post, api_path, json)

    response
  end

  def self.gen_pie id, index_pattern, title, field, references = nil, query = nil
    references = [
      {
        "id": index_pattern,
        "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
        "type": "index-pattern"
      }
    ] if references.blank?
    query = {
      "query": {
        "query": "",
        "language": "kuery"
      }, 
      "filter": [], 
      "indexRefName": "kibanaSavedObjectMeta.searchSourceJSON.index"
    } if query.blank?

    state = {
      "title": title,
      "type": "pie",
      "params": {
        "type": "pie",
        "addTooltip": true,
        "addLegend": true,
        "legendPosition": "top",
        "isDonut": true,
        "labels": {
          "show": false,
          "values": true,
          "last_level": true,
          "truncate": 100
        },
        "dimensions": {
          "metric": {
            "accessor": 1,
            "format": {
              "id": "number"
            },
            "params": {},
            "aggType": "count"
          },
          "buckets": [
            {
              "accessor": 0,
              "format": {
                "id": "terms",
                "params": {
                  "id": "string",
                  "otherBucketLabel": "Other",
                  "missingBucketLabel": "Missing"
                }
              },
              "params": {},
              "aggType": "terms"
            }
          ]
        }
      },
      "aggs": [
        {
          "id": "1",
          "enabled": true,
          "type": "count",
          "schema": "metric",
          "params": {}
        },
        {
          "id": "2",
          "enabled": true,
          "type": "terms",
          "schema": "segment",
          "params": {
            "field": field,
            "orderBy": "1",
            "order": "desc",
            "size": 5,
            "otherBucket": true,
            "otherBucketLabel": "Other",
            "missingBucket": false,
            "missingBucketLabel": "Missing"
          }
        }
      ]
    }

    json = {
      "attributes": {
        "description": "",
        "kibanaSavedObjectMeta": {
          "searchSourceJSON": query.to_json
        },
        "title": title,
        "uiStateJSON": "{}",
        "visState": state.to_json
      },
      "references": references
    }

    api_path = "/api/saved_objects/visualization/#{id}?overwrite=true"

    response = KibanaClient.call(:post, api_path, json)

    response
  end

  def self.gen_graph id, index_pattern, title, type, params, aggs, references = nil, query = nil
    references = [
      {
        "id": index_pattern,
        "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
        "type": "index-pattern"
      }
    ] if references.blank?
    query = {
      "query": {
        "query": "",
        "language": "kuery"
      }, 
      "filter": [], 
      "indexRefName": "kibanaSavedObjectMeta.searchSourceJSON.index"
    } if query.blank?

    state = {
      "title": title,
      "type": type,
      "params": params,
      "aggs": aggs
    }

    json = {
      "attributes": {
        "description": "",
        "kibanaSavedObjectMeta": {
          "searchSourceJSON": query.to_json
        },
        "title": title,
        "uiStateJSON": "{}",
        "visState": state.to_json
      },
      "references": references
    }

    api_path = "/api/saved_objects/visualization/#{id}?overwrite=true"

    response = KibanaClient.call(:post, api_path, json)

    response
  end

  def self.gen_dashboard id, title, panels, references, options = {}, query = nil
    query = {
      "query": {
        "query": "",
        "language": "kuery"
      }, 
      "filter": []
    } if query.blank?

    puts id
    puts title
    puts panels
    puts references
    puts options
    puts query

    json = {
      "attributes": {
        "description": "",
        "hits": 0,
        "kibanaSavedObjectMeta": {
          "searchSourceJSON": query.to_json
        },
        "optionsJSON": options.to_json,
        "panelsJSON": panels.to_json,
        "timeRestore": false,
        "title": title
      },
      "references": references
    }

    puts json 

    api_path = "/api/saved_objects/dashboard/#{id}?overwrite=true"

    response = KibanaClient.call(:post, api_path, json)

    response
  end
end

Generatorから

response = KibanaClient.gen_graph("test_graph", "index_name", "タイトル", "area", params, aggs, references, query)

各変数はKibanaでグラフを作って、Inspectからテンプレ化すると楽

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

RailsとSwiftで動画アップローダーを作る?

まえがき

知らぬ間にまたアドベントカレンダーの季節ですね?
今年もノリと勢いが前のめりすぎて立候補しましたがギリギリです。
(いつになったら一日は30時間になるのか。。)
(ちなみに去年こんなの書きました✌?10分クッキング!誰でもインフルエンサーになれるinstabotの作り方?)

今年のテーマは、動画アップロード機能です。
画像は馴染みがあるけど動画はやったことないなんて人、多くないでしょうか?
そんな私を含めた方達に向けて、ゆるく解説していこうと思います◎

?こんなの作ります
image.png
機能は、動画選択・プレビュー・アップロード・最新の動画をフェッチして再生です。

たぶんレアな記事なので是非お付き合いください?
(あといいね欲しいですね。。!もう押してもらって大丈夫です???)

お勧めしたい人

  • Ruby(Rails)やったことあるよ!
  • HTTPリクエストなんとなくわかるよ!
  • RailsのAPIモードちょっと触ってみたいよ!
  • モバイルアプリやってみたいよ!
  • Swiftやってみたいよ!
  • Swift初心者だよ!
  • 動画アップロードやってみたいよ!
  • というか開いてくれた人 みんなですね!

(せっかく一年に一回のアドベントカレンダーってお祭りなので、みんな読んでね!おねがいします!)

ようこそ〜〜〜?

使用技術

サーバーサイド(API)にRuby
フロントエンド(モバイル)にSwiftの構成です。

Ruby 2.6.2
Ruby on Rails 5.2.3
Swift 5.0.1
Xcode 10.2.1
(ちょっと古いので上げます?)

まだまだやれるぜって人へ
実際にアプリをストアにあげるのに、Railsプロジェクトのデプロイは必須なのでHerikuデプロイとか挑戦してみてください?
【初心者向け】railsアプリをherokuを使って確実にデプロイする方法【決定版】

いざ実装??

全体のザックリした流れ

  1. Swiftで動画アップローダーを作る
  2. SwiftでAPIクライアントを作る
  3. RailsでAPIサーバーを作る
  4. Swiftで動画プレイヤーを作る

~ 完成 ~

1. [Swift] 動画アップローダーの実装?

ザックリした流れ

  1. プロジェクトの作成
  2. シュミレーターにサンプル動画を追加
  3. 今回使用するファイルの説明
  4. 動画選択の機能を作成する
  5. 動画再生の機能を作成する
  6. 動画アップロードの機能を作成する

プロジェクトの作成

  1. Xcodeを起動する
  2. Create a new Xcode projectを選択する
  3. Single View Appを選択してNextを選択する
  4. Product Name(好きなアプリ名)を入力してNextを選択する
  5. 保存先を指定してCreateを選択する

これでSwiftプロジェクトの作成ができました!

シュミレーターにサンプル動画を追加

シュミレーターとは、書いたプログラムの動作を、mac内で確認するためのものです。
(Railsで動作確認する時、ブラウザにlocalhost:3000と入力して見る画面のようなものです。)

シュミレーターには元々サンプル画像しか入っていないので、ここで動画を追加します。
まだ何もコードは書いていませんが起動(ビルド)してみましょう!

  1. 起動したいデバイス(自分はiPhoneXR)を指定して左上の再生ボタンを押します image.png
  2. シュミレーターを起動したら真っ白な画面が出ますがメニューに戻り、写真アプリを開きます
  3. macのfinderから好きな動画を選択します(なければスマホなどからmacに送ってください)
  4. シュミレーターにドラッグアンドドロップで追加します

こんな感じで入ればOK!
image.png

今回使用するファイルの説明

ここから実際に画面を作っていきますが、
先に今回使用するファイルについてザックリ説明しておきます。

  • Main.storyboard(自動生成・自作も可)
    storyboardUI部品(ボタンや画像表示するのに必要なパーツなど)を置いて、
    直感的にレイアウトを作成するファイルです。
    細かい機能やレイアウトを実装はこのファイルでは実装しきれません。あくまでレイアウトをザックリ構築するファイルです。

  • ViewController.swift(自動生成・自作も可)
    storyboardでカバーできない機能的な実装や細かいレイアウトを記述するファイルです。
    例えば、ボタンが押された時の挙動やAPIから受け取ったデータをUIに受け渡す処理なんかを書きます。

今回はVideoUploaderViewController.swiftを作成します。

  • APIClient.swift(自作)
    バックエンド(APIサーバ)へのリクエストやレスポンスを受け取るのに使用します。

  • Info.plist(自動生成・自作も可)
    設定ファイルです。今回はここでフォトライブラリへのアクセス許可の設定をします。

動画選択の機能を作成する

ここでやること
1. VideoUploaderViewController.swiftを作成する
2. Main.storyboardに動画選択用のボタンを追加する
3. ユーザーにカメラロールの使用許可をとる実装を追加する
4. ボタンをViewControllerに接続する
5. ボタンが押されたときにフォトライブラリを開いて動画を選択する
6. 選択された動画のサムネイルをViewControllerに表示する

1. VideoUploaderViewController.swiftを作成する

基本的に、1ページを作るのにUI部品を配置するStoryboard(ViewController)機能を実装するViewControllerがセットで必要です。
はじめに動画の選択からアップロードまでを行うページを作っていきます。
このページは
storyboardに自動生成されるMain.storyboard(ViewController)と、
ViewControllerは自動生成されるViewController.swiftをリネームして作成していきます。

storyboardは自動生成なので、まずはViewController.swiftをリネームていきます。
Xcodeの左側にあるナビゲーターからプロジェクターナビゲーター
(左上のファイルアイコンを押すと出てきます。ファイルの一覧です)を選択し、
ViewControllerをクリックして名前をVideoUploaderViewController.swiftに変更してください。
image.png

ファイル内のclass名も変更します。
image.png

次に、セットで使用するstoryboardに紐づいているViewController名を変更します。
左側のナビゲーターからMain.storyboardを選択します。ここにはViewControllerの白いパーツがあるだけかと思います。
この黄色いアイコンを選択してください。
image.png

右側のインスペクターで、Identity InspectorCustom Classを変更します。
ここを先ほどのVideoUploaderViewController.swiftに変更すると接続が完了します。
image.png

2. Main.storyboardに動画選択用のボタンを追加する

Main.storyboardを開いているのでついでに動画選択のボタンを設置していきます。

Libraryを開いてUIButtonと検索してください。
image.png

これをドラッグアンドドロップでViewController内におきます。
青い淵の状態だとパーツが固定されていないのでUI部品に制約(縦横のサイズやトップからの距離などのルール)を追加していきます。
基本的に必要な制約は、縦横・座標(画面に対しての位置)です。

width/height
widthを設定します。
Ctrを押しながらボタンを選択、ボタン内で選択をやめるとこのようなメニューが出てきます。
image.png
ここでwidthを選択してください。
image.png
赤くなっているのは無視してください。(ちゃんと制約が指定できたら青くなります!)
制約部分を選択すると右側にAttributes Inspectorが開きます。
ここでConstant80にしてください。
image.png

同じようにして、heightのConstantを80で指定してください。
image.png

座標
次にボタンの座標を決めていきます。
画面のトップやボトムから距離を決めたり、画面の真ん中で指定したり方法は色々あります。
今回はトップからの距離と左からの距離を指定します。
先ほどと同じように、Ctrを押しながらボタンを選択、
今度はボタンの外までカーソルを引っ張り選択を解除します。
以下のメニューが出てくるのでTop Space to Safe Areaを選択してください。
image.png
これもConstantを548に変更してください。

同じように画面の左からの制約を付けていきます。
メニューを開いたら、今度はLeading Space to Safe Areaを選択してください。
ここはConstantを71に指定してください。

最後にbuttonという文字をクリックして、Selectに変更してください。
これでボタンの追加は終了です。(好きに色とか付けてもらったり、位置も自由で大丈夫です。)
(指定に中途半端な値を指定しているのは、デモで作ったアプリを
AutoLayoutで実装しているのですが、そのレイアウトに近づけるためです。(起動に使う端末はXRです。)
AutoLayoutとは、UI部品同士を相対的に配置して、どのサイズの端末でも同じような配置でレイアウトを実現できる機能です。
後でソースコードを載せるので参考にしてみてください??‍♀️)
image.png

3. ユーザーにカメラロールの使用許可をとる実装を追加する

カメラアプリをインストールした時にこのようなモーダルに遭遇することがあると思います。
image.png
ユーザーから、フォトライブラリにアクセスする許可を取らないと、
動画を選択することができないのでこれを実装していきます。

まず、Info.plistでフォトライブラリを使用する利用目的を設定します。
アプリ名のディレクトリ以下のInfo.plistを開いてください。(テスト用のディレクトリにもあるので注意してください)
Information Property Listの横にある+ボタンを選択して、Privacy - Photo Library Usage Descriptionを入力してください。
行が追加されたら、Valueの部分には利用目的を記述します。
image.png

ここまででInfo.plistの設定は終わりです。

次に、アラートを出す実装をします。
iOS11以降.plistに設定してもアクセス権限の確認が自動的に行われなくなったので
意図的にタイミングを指定してアラート表示する必要があります。
最初の画面が表示されたタイミングでアラートを表示するようにしていきましょう。

最初に開かれるページになる、VideoUploaderViewController.swiftを開いてください。

今回必要なフォトライブラリへアクセスするのでPhotosというフレームワークを使用します。
Appleが用意してくれているものなのでimport Photosを記述することで使用できるようになります。
以下のように追加してください。

VideoUploaderViewController.swift
import UIKit
import Photos

自動生成されるviewDidLoadの下に、confirmPhotoLibraryAuthentication()という関数を作成します。
ここで、フォトライブラリの使用許可の確認を行います。

VideoUploaderViewController.swift
    private func confirmPhotoLibraryAuthentication() {

    }

privateは、このclass内でのみ呼び出しを許可したい時に使用します。
(railsでもストロングパラメーターを記述する時に使ったことがあるかと思います。同じやつです。)

この関数内で、アクセス許可をされているかの現状の確認許可されていなかった時の挙動を記述していきます。

VideoUploaderViewController.swift
    private func confirmPhotoLibraryAuthenticationStatus() {
        //権限の現状確認(許可されているかどうか)
        if PHPhotoLibrary.authorizationStatus() != .authorized {
            //許可(authorized)されていない・ここで初回のアラートが出る
            PHPhotoLibrary.requestAuthorization { status in
                switch status {
                //もし状態(status)が、初回(notDetermined)もしくは拒否されている(denied)の場合
                case .notDetermined, .denied:
            //許可しなおして欲しいので、設定アプリへの導線をおく
                    self.appearChangeStatusAlert()
                default:
                    break
                }
            }
        }
    }

PHPhotoLibrary.requestAuthorizationの時に先ほどのアラートが出現します。
その結果をstatusとしてクロージャー({})内に展開します。
このアラートは基本的に一度きりなので
アプリを閉じられたりして初回(notDetermined)のままだったり
アクセスを許可していない(denied)ユーザーに対して再度設定を促すアラートを出そうと思います。

appearChangeStatusAlert()という関数を作成します。
confirmPhotoLibraryAuthenticationStatus関数の下に追加してください。

VideoUploaderViewController.swift
    private func confirmPhotoLibraryAuthenticationStatus() {
        //略
    }

   //ここから
    private func appearChangeStatusAlert() {
        //フォトライブラリへのアクセスを許可していないユーザーに対して設定のし直しを促す。
        //タイトルとメッセージを設定しアラートモーダルを作成する
        let alert = UIAlertController(title: "Not authorized", message: "we need to access photo library to upload video", preferredStyle: .alert)
        //アラートには設定アプリを起動するアクションとキャンセルアクションを設置
        let settingAction = UIAlertAction(title: "setting", style: .default, handler: { (_) in
            guard let settingUrl = URL(string: UIApplication.openSettingsURLString) else { return }
            UIApplication.shared.open(settingUrl, options: [:], completionHandler: nil)
        })
        let closeAction = UIAlertAction(title: "cancel", style: .cancel, handler: nil)
        //アラートに上記の2つのアクションを追加
        alert.addAction(settingAction)
        alert.addAction(closeAction)
        //アラートを表示させる
        self.present(alert, animated: true, completion: nil)
    }

ここまで書けたら、画面が読み込まれた際に呼び出されるviewDidLoad()から
confirmPhotoLibraryAuthenticationStatus()を呼び出してみましょう。

VideoUploaderViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()
        // MARK: allow access to camera roll
        self.confirmPhotoLibraryAuthenticationStatus()
    }

実際に起動して、動作を確認してみましょう。
Info.plistで指定した利用目的はここに出てきます!
image.png

Don't Allowを押すと先ほどタイトルなどを設定したアラートが出現します。
settingを押すと設定アプリも開けるようになってると思います。

image.png

デバッグの注意点:
今回の実装では最初のアラートは初回起動時一回しか出てこないので、
再度確認したいときは、シュミレーターからアプリを削除してビルドし直してください。

4. ボタンをViewControllerに接続する

ユーザーからフォトライブラリへのアクセス許可を貰えるようになったので、
ここからはフォトライブラリから動画を選択する機能を作っていきます。

まずVideoUploaderViewController.swiftと先ほどstoryboardに配置したボタンを接続していきます。
Main.storyboardを開いてください。
optionを押しながら、VideoUploaderViewController.swiftを開きます。
左側にMain.storyboard、右側にVideoUploaderViewController.swiftが開けていればOKです。
image.png

storyboardでCtrを押しながらボタンを選択して、ViewcontrollerのviewDidLoadの上まで
カーソルを引っ張ってください。
選択をやめると、このようなメニューが出てくるかと思います。
image.png
以下のようにConnectionActionに変更して、
NamedidTapSelectButtonと入力してConnectを押してください。
image.png
ConnectionActionを指定することで
ボタンをタップされた後に呼び出される関数を自動生成することができます。
image.png

ここまでで、VideoUploaderViewController.swiftと動画選択のボタンの接続は終わりです。

5. ボタンが押されたときにフォトライブラリを開いて動画を選択する

ここでやることは、UIImagePickerControllerを使って
フォトライブラリから動画を選択できるようにします。

UIImagePickerControllerとは、
メディア周り(フォトライブラリにアクセスしたりやカメラを起動したり)の機能を簡単に扱えるようにするクラスです。Photosフレームワークをインポートしてあるだけで使用できます。
(今回はフォトライブラリのアクセス許可をとる時に既にインポートしてあるのですぐに使えます。)

早速実装していきます。
VideoUploaderViewControllerクラスに、UIImagePickerControllerインスタンスを生成します。

VideoUploaderViewController.swift
class VideoUploaderViewController: UIViewController{

    let imagePickerController = UIImagePickerController()
    //略

}

次に、VideoUploaderViewController.swiftにselectVideo関数を作成します。
ここにフォトライブラリを開いて動画を選択する機能を実装していきます。

VideoUploaderViewController.swift
    private func appearChangeStatusAlert() {
        //略
    }
    //ここから
    private func selectVideo() {
        //選択できるメディアは動画のみを指定
        self.imagePickerController.mediaTypes = ["public.movie"]
        //選択元はフォトライブラリ
        self.imagePickerController.sourceType = .photoLibrary
        //実際にimagePickerControllerを呼び出してフォトライブラリを開く
        self.present(self.imagePickerController, animated: true, completion: nil)
    }

最後に、自動生成したdidTapSelectButtonの関数からselectVideo()を呼び出すことで
ボタンがタップされた時にフォトライブラリを開き動画を選択することができるようになります。

VideoUploaderViewController.swift
   //選択ボタンがタップされた時に呼び出される
    @IBAction func didTapSelectButton(_ sender: Any) {
        selectVideo()
    }

ビルドして確認してみましょう。動画を選択できるようになったと思います。
image.png

6. 選択された動画のサムネイルをViewControllerに表示する

最後に選択した動画のサムネイルをVideoUploaderViewControllerに表示します。
少しやることが多いので、流れをまとめます。以下の通りです。
- Main.storyboardにサムネイルを表示するためのUIImageViewを置く
- UIImageViewをViewControllerに接続する
- ViewControllerをUIImagePickerControllerDelegateとUINavigationControllerDelegateに批准させる
- imagePickerControllerが閉じられる時に呼び出される関数を実装する
- 取得した動画のパスを元にサムネイルを生成する

Main.storyboardにサムネイルを表示するためのUIImageViewを置く

Main.storyboardを開いてください。
今回はサムネイル(画像)を表示することが目的です。画像を表示するにはUIImageViewを使用します。
UIButtonの時と同じように、LibraryからUIImageViewを検索して追加します。
高さはConstantを250ポイントに指定してください。
横幅は画面いっぱいに指定してください。
画面いっぱいにするには、Ctrを押しながらUIImageViewを選択してカーソルをUIImageViewの外側まで持っていきます。
ここで選択を解除すると以下のメニューが出てきます。
image.png
Equal Widthsを選択すると画面に対して横幅がいっぱいになります。

次に横軸の座標を指定します。今回は画面に対して中央にします。
同様にメニューを出してCenter Horizontally in Safe Areaを指定してください。
このように真ん中にUIImageViewが移動すると思います。
image.png
最後に縦軸の座標を指定します。今回は動画選択のボタンのトップよりUIImageViewのボトムが54ポイント上になるように指定します。
Ctrを押しながら、UIImageViewを選択してください。カーソルは選択ボタンまで持っていってメニューを表示させます。
ここでVertical Spaceを選択してください。
image.png
Constantは54ポイントを指定してください。

これでUIImageViewが置けました。
image.png

UIImageViewをViewControllerに接続する

選択ボタンの時と同じようにMain.storyboardを開いて、隣にVideoUploaderViewcontroller.swiftを開いてください。
(optionを押しながらVideoUploaderViewcontroller.swiftを選択してください)
Ctrを押しながら、UIImageViewをVideoUploaderViewcontroller.swiftに繋いでください。
今度はConnectionOutletのままで、Connectしてください。
image.png
以下のコードが生成されたら接続は完了です。
image.png

ViewControllerをUIImagePickerControllerDelegateとUINavigationControllerDelegateに批准させる

ここが一番わかりづらいと思います?
そもそもDelegate(デリゲート)とはSwiftでよく使われる考え方で、ここら辺の記事がわかりやすいです。
【swift】イラストで分かる!具体的なDelegateの使い方。

簡単にいうと、あるクラスが別のクラスに処理をまかせる(委譲する)ことです。
なんで批准という表現が使われるかという話は、例え話がわかりやすいです。
今回扱うデリゲートやプロトコルは、所謂条約のような決まり事です。
必ず守らなければならないルールがあったり、出来ることが増えたりします。
またそこには加盟する国々(ViewControllerなど)がいます。
この関係を国々が条約に批准するというように、
ViewControllerもデリゲートやプロトコルに批准すると考えると理解しやすいです!(個人談)

今回は、デリゲートに批准することで、VideoUploaderViewControllerに
UIImagePickerControllerの関数を使って、選択した動画やサムネイルを受け取ってもらいます。

実装方法は、まずクラスを定義している部分にUIViewControllerと同じように必要なDelegateを記述します。
image.png

次に、imagePickerControllerの代わりにVideoUploaderViewControllerが役割を肩代わりしますよ!って宣言をします。

VideoUploaderViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()
        self.imagePickerController.delegate = self
    //略

これで批准完了です!

imagePickerControllerが閉じられる時に呼び出される関数を実装する

まず、選択された動画のurlを保持するための変数を定義します。

VideoUploaderViewController.swift
class VideoUploaderViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    //ここを追加
    private var videoUrl: NSURL?

先ほどのデリゲートに批准したので、動画を選択した後imagePickerが閉じられる時に呼び出される関数を利用できるようになります。

VideoUploaderViewController.swift
   //imagePickerが閉じられる時に呼ばれる
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
     //キーを指定して選択された動画のパスを取得する
        let key = UIImagePickerController.InfoKey(rawValue: "UIImagePickerControllerMediaURL")
        videoUrl = info[key] as? NSURL
     //動画の絶対パスを元にサムネイルを生成(generateThumbnailFromVideoの実装は後述します。)
        //先ほど接続したthumbnailImageViewのimageにサムネイルをセット
        thumbnailImageView.image = generateThumbnailFromVideo((videoUrl?.absoluteURL)!)
     //サムネイルの縦横比を変えずに長い辺を画面サイズに合わせる設定
        thumbnailImageView.contentMode = .scaleAspectFit
     //imagePickerControllerを閉じる
        imagePickerController.dismiss(animated: true, completion: nil)
    }

取得した動画のパスを元にサムネイルを生成する

先ほど後述すると書いたgenerateThumbnailFromVideoという関数を実装します。

VideoUploaderViewController.swift
    private func generateThumbnailFromVideo(_ url: URL) -> UIImage? {
        //以下の3行で縦動画から画像を取り出しても横向きの画像にならないようにしてる
        let asset = AVAsset(url: url)
        let imageGenerator = AVAssetImageGenerator(asset: asset)
        imageGenerator.appliesPreferredTrackTransform = true
        //切り取るタイミングの指定
        var time = asset.duration
        time.value = min(time.value, 2)
     //サムネイルの生成
        do {
            let imageRef = try imageGenerator.copyCGImage(at: time, actualTime: nil)
            return UIImage(cgImage: imageRef)
        } catch {
            return nil
        }
    }

この関数は-> UIImage?でサムネイルを返り値として指定しています。
生成に成功するとreturn UIImage(cgImage: imageRef)で画像を返してくれます。
これが先ほど実装した関数のこの部分に返るのでサムネイルが表示されるようになります。
thumbnailImageView.image = generateThumbnailFromVideo((videoUrl?.absoluteURL)!)

ビルドして確認してみましょう!
動画選択後にサムネイルが表示されてればOKです。
image.png

動画選択の機能は以上です。

動画再生の機能を作成する

ここでやること
1. 動画再生用ボタンを設置する
2. ViewControllerに接続する
3. 動画再生の関数を実装する

1. 動画再生用ボタンを設置する

選択ボタンの時と同じようにMain.storyboardに動画再生用のボタンを設置してください。
・横幅と高さは選択ボタンと一緒でconstantを80ポイント
・縦の座標は選択ボタンのcenterと同じ
・横の座標は画面に対してcenter
こんな感じです(急に色付きでスミマセン?)
image.png

2. ViewControllerに接続する

こちらも選択ボタンと同じようにVideoUploaderViewControllerに接続してください。
以下の関数ができればOKです!
image.png

3. 動画再生の関数を実装する

動画を再生するにあたって、再生プレーヤーが必要となります。
Appleが提供しているAVKitフレームワークに、
ビデオコンテンツを再生するためのインターフェイスが用意されているのでこれを使用していきます。

VideoUploaderViewController.swift
import UIKit
import Photos
//ここを追加
import AVKit
VideoUploaderViewController.swift
    private let imagePickerController = UIImagePickerController()
    //ここを追加
    private let playerViewController = AVPlayerViewController()

次に、動画再生を行うplayVideo(from url: URL)を実装します。

VideoUploaderViewController.swift
    private func playVideo(from url: URL) {
        //プレイヤーに受けとったurlをセット
        let player = AVPlayer(url: url)
     //先ほど初期化したplayerViewControllerのプレイヤーに上記のプレイヤーをセット
        playerViewController.player = player
     //playerViewControllerの表示・再生
        self.present(playerViewController, animated: true) {
            print("playing video")
            self.playerViewController.player!.play()
        }
    }

最後に再生ボタンを押された後にplayVideo(from url: URL)を呼び出します。

VideoUploaderViewController.swift
    @IBAction func didTapPlayButton(_ sender: Any) {
        //選択された動画の絶対パスがオプショナル(nilの可能性がある)ので
     //guard(railsでいうunless)でパスがnilなら早期リターンにしてる
        guard let url = videoUrl?.absoluteURL else { return }
        playVideo(from: url) }

ここまで実装できたら、ビルドして再生ボタンを押して確認してみましょう!

動画再生の機能は以上です。

動画アップロードの機能を作成する

ここでやること
1. 動画アップロード用のボタンを設置する
2. ViewControllerに接続する
3. 動画アップロードの関数を実装する

1. 動画アップロード用のボタンを設置する

選択ボタンの時と同じようにMain.storyboardに動画アップロード用のボタンを設置してください。
・横幅と高さは選択ボタンと一緒でconstantを80ポイント
・縦の座標は選択ボタンのcenterと同じ
・横の座標は画面に右端対してconstantを71ポイント
こんな感じです。
image.png

2. ViewControllerに接続する

こちらも選択ボタンと同じようにVideoUploaderViewControllerに接続してください。
以下の関数ができればOKです!
image.png

3. 動画アップロードの関数を実装する

動画のアップロードを行うuploadVideo()を実装します。
今回アップロードするのは動画のみです。
アップロードに必要な情報は、
・選択された動画のurl
・選択された動画の名前
になります。

VideoUploaderViewController.swift
    private func uploadVideo() {
     //urlと名前がなければ早期リターンさせる
        guard
            let videoClipPath = videoUrl?.absoluteURL,
            //urlの最後がファイル名になる
            let videoClipName = videoUrl?.lastPathComponent
        else { print("not found video path or name"); return }
        //バックエンドにリクエストを送る
     //ここは次の章で実装するので(APIクライアント)エラーのままで大丈夫です。
        API.postData(videoClipPath: videoClipPath, videoClipName: videoClipName)
    }

uploadVideo()もアップロード用のボタンが押された時に呼び出されるようにしておきます。

VideoUploaderViewController.swift
    @IBAction func didTapUploadButton(_ sender: Any) { uploadVideo() }

この機能は、APIクライアントとバックエンドのAPIを作って完成します。

2. [Swift] APIクライアントの実装?

SwiftからRailsにデータを送信するための機能を作っていきます。が、
その前に。

動画データの扱い方について

一回やったのですが動画変換はbase64だと重くてツライので、
APIのリクエストヘッダーのContent-Typeにmultipart/form-dataを指定して
動画をAppleの標準の拡張子.MOVのままバックエンドにアップロードできるように実装します。

ザックリと用語の解説

base64

base64はバイナリーデータ(今回は動画)を
String(ASCIIテキスト: アスキーと読みます)に変換する方法です。
バイナリーデータをStringに変換できるのでjson形式で扱えて便利です。
有名な変換方法なので詳しい話はこちらをどうぞ!
base64ってなんぞ??理解のために実装してみた

弱点は、上でも書いた通りめちゃくちゃに重たいです。
理由はASCIIテキストに変換していく中でデータサイズが33%増加することです。
例えば、1枚66KBのサンタを変換するとこんな感じ
(ちなみに文字列はまだまだ続く。
base64は100MB未満のデータに適していると言われているのでこのサンタはちょろい方)
??base64エンコーダー
image.png
こんな感じなので動画を全部文字列で扱ったらbase64⇄動画の変換は地獄です。
(1分くらいの動画をbase64でコンソールに出力したらXcodeが固まった)

ということで今回は不採用?

multipart/form-data

HTML4から導入された方法で、複合型のコンテンツをMIME Typeを指定することで形式を変えずに送信することができます。(textだったりjpegだったりいろんな形式のデータを一緒に送信することが可能ということ)
HTTPリクエストのヘッダー部分で指定するContent-typeの一種です。
(Content-typeとは「このリクエストの中身はこんな形式のデータが入ってますよ〜」って宣言。)
詳しくはこちら。[フロントエンド] multipart/form-dataを理解してみよう
今回送信するデータはvideo/quicktime(.MOVファイル)だけですが、拡張性があるのでmultipart/form-dataを指定しています。

ザックリした流れ

  1. Alamofireのインストール
  2. APIClient.swiftの作成
  3. POSTリクエストを実装
  4. http通信の許可

Alamofireのインストール

APIクライアントを簡単に実装できるライブラリです。CocoaPodsでインストールします。
導入方法はこちらが詳しいので参考にしてください。(【Swift】CocoaPods導入手順)[https://qiita.com/ShinokiRyosei/items/3090290cb72434852460]
今回pod 'Alamofire', '~> 4.7.2'を指定してください。
インストール後、Xcodeを閉じて
プロジェクトディレクトリ以下に出てくる.xcworkspaceファイルで開き直したら完了です。
image.png

APIClient.swiftの作成

Xcodeが開けたら、左側に表示されるナビゲーターからプロジェクト名のディレクトリを指定してください。
?この状態
image.png
左下の+ボタン > File... > Swift File の順で、APIClient.swiftというファイルを作ってください。
image.png
ナビゲーターのプロジェクト名のディレクトリ以下にファイルが追加されていたら完了です。

POSTリクエストを実装

Alamofireを使って、APIClient.swiftにVideoのPOSTリクエストを書いていきます。
まずはAlamofireをインポートします。

APIClient.swift
import Alamofire

APIクライアントを書いていきます。
ここでやりたいのは、
- Content-type: multipart/form-dataでリクエストを作成
- 動画データの追加
- 送信(エラーハンドリング)
です。

APIClient.swift
struct API {
    //APIのエンドポイント。
    static let baseUrl = URL(string: "http://localhost:3000/api/v1/videos")!
    static func postData(videoClipPath: URL, videoClipName: String){
        //multipart/form-dataでデータを送信する
        Alamofire.upload(multipartFormData: { multipartFormData in
            //multipartFormDataオブジェクトに対してデータの追加を行う
            //withNameはrailsのActiveStorage側で保存するときのキーと同じ
            multipartFormData.append(videoClipPath, withName: "clip", fileName: videoClipName, mimeType: "video/quicktime")
        }, to: baseUrl) { encodingResult in
            //encodingが成功するとこのハンドラが呼ばれる
            switch encodingResult {
            case.success(let upload, _ ,_):
                print(upload)
                upload
                    .uploadProgress(closure: { (progress) in
                        //進捗率の取得
                        print("Upload Progress: \(progress.fractionCompleted)")
                    })
            case.failure(let error):
                print(error)
            }
        }
}

こんな感じでPOSTリクエストの実装は完了です。

http通信の許可

http通信を許可する
iOS9以降、意図的にドメインを許可する設定をしないとXcodeでhttp通信できなくなりました。
設定しないと以下のようなエラーを吐きます。。

The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.

今回、rails側のlocalhostを叩きたいのでこの設定が必要になります。
この記事がわかりやすいので設定してみてください??
【swift】XcodeでiOSアプリのhttp通信を許可する方法
[おまけ]
ここではバックエンドやフロントエンドでCORSを指定する時でいう
ワイルドカードの指定と同じようなことをやっているので(全ドメインを許可)
本番環境で必要になる場合は(herokuデプロイとか)Exception Domainで特定のドメインのみを許可してください。
iOS9でHTTP通信ができない時の解決法

以上でAPIクライアントの実装は終わりです!

3. [Rails] APIサーバーの実装?

ザックリした流れ

  1. プロジェクトの作成
  2. モデルの作成
  3. コントローラーの作成
  4. ルーティングの作成

プロジェクトの作成

環境構築は割愛??
1. ターミナルを開いて$ cd /path/to/プロジェクトを作成したいディレクトリで移動します
2. $ rails _5.2.3_ new video_uploader_server --api --skip-testを実行します(アプリ名はvideo_uploader_serverを好きな名前に変更してください)

オプション 内容
_5.2.3_ railsのバージョン指定(6以降だと色々面倒なので今回はこちら)
--api APIモード
--skip-test 今回テスト書きません。書く方は抜いてください

その他optionはrails newの書き方について徹底解説!を確認してみてください。

モデルの作成

モデルを作成します。
今回必要なのは
- Videoモデル
- ActiveStrangeで使用するモデル
です。

ActiveRecordのVideoモデルの作成

  1. $ cd アプリ名作成したプロジェクトに移動
  2. $ rails g model videoでVideoモデルを作ります(gはgenerateのgです。今回カラム使わないのでこれだけで大丈夫です。)

ActiveStorageの設定を行うのでマイグレーションはまだ行わなくて大丈夫です!
続きをどうぞ!

ActiveStorageの設定

今回扱うのは動画(.MOV)なのでActiveStorageを使って保存していきます。
Active Storage の概要に詳しく解説されているので参考にしてください。
1. $ rails active_storage:install
ログはこんな感じになります
image.png
2. $ rails db:migrateでデータベースを作ります
3. お好きなエディタでプロジェクトファイルを開きます
4. video.rbを開いて編集します

Videoモデルに紐付ける動画ファイルをclipという名前でActiveStorageから呼び出せるように指定します。

video.rb
class Video < ApplicationRecord
    #ここを追加
    has_one_attached :clip
end

これはActiveStoregeで使用するモデルとVideoモデルのリレーションを定義しています。
Videoモデルにカラムを追加しなくてもVideoモデルに動画を保存しているかのように
ActiveStorageで動画を保存することができるようになります。

コントローラーの作成

  1. $ rails g controller api/v1/videosでコントローラーを作成します
    v1はバージョン1という意味です。
    このように階層を分けてAPIのバージョンを管理するプロジェクトが多いです。
    今回は簡単シンプルな構成で機能追加も想定していないので無くても大丈夫です。

  2. videos_controller.rbを編集します

videos_controller.rb
class Api::V1::VideosController < ApplicationController
    # videoの保存
     def create
        video = Video.save(video_params)
        if video.save
            render json: { status: 'ok' }
        else
            render json: { status: 'ng' }
        end
    end
    # videoの取得
    def index
        # とりあえず最後に保存したもの一件だけを表示
        video = Video.last.clip
        # ActiveStorageで保存したビデオのurlをjsonで返す
        url = url_for(video)
        render json: { url: url }
    end

    private 
    def video_params
        params.permit(:clip) #clipはActiveStorageに保存する時のキー
    end
end

ルーティングの作成

controllerの階層を考慮した上でvideos_controllerのルーティングが設定できれば問題ないです。

routes.rb
Rails.application.routes.draw do
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  namespace 'api' do
    namespace 'v1' do
      resources :videos
    end
  end
end

こんな感じ。今回使わないアクションが多いのでonlyで絞り込んでもらってもいいです。
image.png

これでルーティングの実装も終わりです。
Xcodeでアプリをビルドして、railsもサーバーを起動してアップロードの確認をしてください。

[ちなみに]
swiftからのリクエストは、rails側のコンソールでこんな感じで受け取ってるのを確認できます。

Parameters: {"data"=>"testVideo", "clip"=>#<ActionDispatch::Http::UploadedFile:0x00007f982a2e9c70 @tempfile=#<Tempfile:/var/folders/rk/n8_pgb7x3j18qzjq1g4_4j380000gn/T/RackMultipart20191204-82817-n4evni.MOV>, @original_filename="34153FC2-F496-4B63-B0EC-D005AA1BC8DB.MOV", @content_type="video/quicktime", @headers="Content-Disposition: form-data; name=\"clip\"; filename=\"34153FC2-F496-4B63-B0EC-D005AA1BC8DB.MOV\"\r\nContent-Type: video/quicktime\r\n">}

ターミナルで
open /var/folders/rk/n8_pgb7x3j18qzjq1g4_4j380000gn/T/RackMultipart20191204-82817-n4evni.MOV で送られてきた動画の再生ができたりします。

4. [Swift] 動画プレイヤーの実装?

ザックリした流れ

  1. 最新の動画再生用ボタンを設置する
  2. ViewControllerに接続する
  3. 最新の動画をフェッチするリクエストを実装する
  4. 最新の動画を再生する

動画アップロード用のボタンを設置する

選択ボタンの時と同じようにMain.storyboardに動画アップロード用のボタンを設置してください。
・横幅は文字の長さ。高さは文字の大きさを24ポイント
・縦の座標は再生ボタンから42ポイント下
・横の座標は画面に右端対してcenter
こんな感じです。
(画像追加します!)

ViewControllerに接続する

こちらも選択ボタンと同じようにVideoUploaderViewControllerに接続してください。
以下の関数ができればOKです!
(画像追加します!)

最新の動画をフェッチするリクエストを実装する

APIClient.swiftに以下を追加してください。

APIClient.swift
    //completionを使うことで呼び出し側でリクエストと動画再生を同期的に扱えるようにしてる
    static func fetchLatestVideoUrl(completion: @escaping (URL) -> ()) {
        //レスポンスの型。今回はurlのみ
        struct FetchResult: Codable {
            let url: String
        }
        //今回パラメーターは特に必要ないので[:](空)で
        Alamofire.request(baseUrl, method: .get, parameters: [:])
            .responseJSON { response in
                switch response.result {
                case .success:
                    print("Success!")
                    //レスポンスをFetchResultに変換する
                    guard
                        let data = response.data,
                        let result = try? JSONDecoder().decode(FetchResult.self, from: data),
                        //取得できたFetchResultオブジェクトのurl(String?)からURLを生成
                        let fetchedUrl = URL(string: result.url)
                    else { return }
                    //取得できたURLをcompletionに渡す
                    completion(fetchedUrl)
                case .failure:
                    print("Failure!")
                }
        }
    }

最新の動画を再生する

最新の動画を再生するための関数playUploadedLatestVideo()を実装します。

VideoUploaderViewController.swift
    private func playUploadedLatestVideo() {
        //バックエンドからファイルのurlを返してもらう(http://localhost:3000で始まるもの)
     //先ほどのcompletionはここの{}(クロージャ)。この中でurlを受け取る
        API.fetchLatestVideoUrl() { url in
            //このurlを使ってプレビューと同様にplayVideo(from url: URL)でビデオを開く
            self.playVideo(from: url)
        }
    }

最後にボタンが押されたタイミングでplayUploadedLatestVideo()を呼び出さすように記述して完成です!

VideoUploaderViewController.swift
    @IBAction func didTapLatestVideoButton(_ sender: Any) { playUploadedLatestVideo() }

あとがき

お疲れ様でした!
今回は最低限の機能実装でした。
エラーハンドリングしてアラート出したり、一覧ページを作ったり、
削除機能つけたりお好みでいろいろ追加してみてください!

あとそもそもアプリをストアにあげるとかですね!

気になったこととかもっといいやり方あるよとかご指摘は
お気軽にバシバシください??

読んでくださりありがとうございました?

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

山手線で堂々とお気に入り風俗嬢の出勤情報を確認できるステルスサービス「Cyber Office クラウド」

スクリーンショット 2019-12-05 10.03.07.png
スクリーンショット 2019-12-05 10.00.42.png

1 解決すべき課題

ぼく「私はオキニ嬢が決まっている。その出勤を確認したいだけなんだ・・・・。」
でも電車では風俗サイトなんて見れない、自宅でも家族がいるし、嫁は相手にしてくれない、でも独りの時間にそんなサイトばっかり見て時間が過ぎ去るのは嫌だ!

私が必要とするサービスを自分のために作ってみました。

1:見る場所問題
風俗関係のウェブサイトってどうしても見る場所を選びますよね。じっくり新人や新店舗を見たい人に関しては、専用サイトで見る必要がありますしそれでいいと思います。しかし、すでにお気に入り嬢が決まっていてその娘達の出勤だけ見たいという人には、XXXサイトを行き来するのは大変な苦労です。

当然家族団欒の場や電車などの公共の場では見る事ができません。

2:時間の浪費問題
私はお気に入り嬢の出勤だけを見たいだけだったのに、気がつけばエロサイトをネットサーフィンしてしまい、大変な時間を費やしたことがあります。サクッと出勤だけ確認できれば他の事にも時間が使えると思いました。

3:見た目の問題
掲載されているイメージがやはりセクシーすぎて、1の問題を引き起こします。もちろん1人で見る場合にはいいのですが、公共の場には向きません。

以上の課題を解決したいと思います。

2-1 設計案

・ どこで見てても良いUI
・ 正確なお気に入り嬢の出勤情報を横断的に捕捉

簡単ですがひとまずこれを目標にして制作しました。

2-2 使用技術と骨子

・正確な嬢の出勤情報は大手風俗情報サイトよるとも[Yorutomoさん]からスクレイピングして、データを解析する事にしました。

・フォームにyorutomoの嬢の個別ページを入力するだけで、自分だけのオキニ嬢出勤表が作れるようにしました。

・ 使用技術
Ruby on rails / Heroku / nokogiri / mechanize / Boot strap /devide

3 使い方

・1:ビジネス管理アプリ風Uiにログインします。Deviseで管理画面を作っています。今のところ、confirmableは切っていますので、適当なメールアドレスでも登録できるでしょう。

スクリーンショット 2019-12-05 9.41.04.png

2:上部メニュービジネスメンバーの追加からオキニ嬢のURLをよるともからコピペします。

スクリーンショット 2019-12-05 9.32.21.png

スクリーンショット 2019-12-05 9.44.04.png

よるとも側のページは個別の嬢の詳細ページでURLは以下のような感じのはずです。
https://www.yorutomo.net/tokyo/m2/a6/shop"店番号"/gal"女性番号"/

3:いくつか自分のお気に入り嬢を登録すると店舗横断型のスケジュール表が完成します。
スクリーンショット 2019-12-05 9.32.21.png

ユーザーインターフェイスは極力風俗関係の色を排除し、嬢の名前も初めの一文字のみの表示としています。なお迷いましたが、やはり予約はこのまましたいので電話番号は表示するようにしました。

「ペン」アイコンで編集・「ゴミ箱アイコンで」削除が可能です。

4 良かった事と課題

・ 電車でも人目を気にせずお気に入り嬢の出勤を確認できるようになった。
・ 公式サイト、風俗情報サイトをサーフィンしまくる時間が削減できた。

[課題]
・ スマホにしっかり対応するUl設計。
・ 嬢の名前をはっきりと出すとステルス効果が薄れるので、現在は一文字表示にしているが良い方法はないものか。

もし需要があればURL公開しますので使ってみてください。技術としては今あるものを重ね合わせた一般的なもので作っています。

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

どこでも堂々とお気に入り風俗嬢の出勤情報を確認できるステルスサービス「Cyber Office クラウド」

スクリーンショット 2019-12-05 10.03.07.png
スクリーンショット 2019-12-05 10.00.42.png

1 解決すべき課題

ぼく「私はオキニ嬢が決まっている。その出勤を確認したいだけなんだ・・・・。」
でも電車では風俗サイトなんて見れない、自宅でも家族がいるし、嫁は相手にしてくれない、でも独りの時間にそんなサイトばっかり見て時間が過ぎ去るのは嫌だ!

私が必要とするサービスを自分のために作ってみました。

1:見る場所問題
風俗関係のウェブサイトってどうしても見る場所を選びますよね。じっくり新人や新店舗を見たい人に関しては、専用サイトで見る必要がありますしそれでいいと思います。しかし、すでにお気に入り嬢が決まっていてその娘達の出勤だけ見たいという人には、XXXサイトを行き来するのは大変な苦労です。

当然家族団欒の場や電車などの公共の場では見る事ができません。

2:時間の浪費問題
私はお気に入り嬢の出勤だけを見たいだけだったのに、気がつけばエロサイトをネットサーフィンしてしまい、大変な時間を費やしたことがあります。サクッと出勤だけ確認できれば他の事にも時間が使えると思いました。

3:見た目の問題
掲載されているイメージがやはりセクシーすぎて、1の問題を引き起こします。もちろん1人で見る場合にはいいのですが、公共の場には向きません。

以上の課題を解決したいと思います。

2-1 設計案

・ どこで見てても良いUI
・ 正確なお気に入り嬢の出勤情報を横断的に捕捉

簡単ですがひとまずこれを目標にして制作しました。

2-2 使用技術と骨子

・正確な嬢の出勤情報は大手風俗情報サイトよるとも[Yorutomoさん]からスクレイピングして、データを解析する事にしました。

・フォームにyorutomoの嬢の個別ページを入力するだけで、自分だけのオキニ嬢出勤表が作れるようにしました。

・ 使用技術
Ruby on rails / Heroku / nokogiri / mechanize / Boot strap /devide

3 使い方

・1:ビジネス管理アプリ風Uiにログインします。Deviseで管理画面を作っています。今のところ、confirmableは切っていますので、適当なメールアドレスでも登録できるでしょう。

スクリーンショット 2019-12-05 9.41.04.png

2:上部メニュービジネスメンバーの追加からオキニ嬢のURLをよるともからコピペします。

スクリーンショット 2019-12-05 9.32.21.png

スクリーンショット 2019-12-05 9.44.04.png

よるとも側のページは個別の嬢の詳細ページでURLは以下のような感じのはずです。
https://www.yorutomo.net/tokyo/m2/a6/shop"店番号"/gal"女性番号"/

3:いくつか自分のお気に入り嬢を登録すると店舗横断型のスケジュール表が完成します。
スクリーンショット 2019-12-05 9.32.21.png

ユーザーインターフェイスは極力風俗関係の色を排除し、嬢の名前も初めの一文字のみの表示としています。なお迷いましたが、やはり予約はこのまましたいので電話番号は表示するようにしました。

「ペン」アイコンで編集・「ゴミ箱アイコンで」削除が可能です。

4 良かった事と課題

・ 電車でも人目を気にせずお気に入り嬢の出勤を確認できるようになった。
・ 公式サイト、風俗情報サイトをサーフィンしまくる時間が削減できた。

[課題]
・ スマホにしっかり対応するUl設計。
・ 嬢の名前をはっきりと出すとステルス効果が薄れるので、現在は一文字表示にしているが良い方法はないものか。

もし需要があればURL公開しますので使ってみてください。技術としては今あるものを重ね合わせた一般的なもので作っています。

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

堂々とお気に入り風俗嬢の出勤情報を確認できるステルスサービス「Cyber Office クラウド」

スクリーンショット 2019-12-05 10.03.07.png
スクリーンショット 2019-12-05 10.00.42.png

1 解決すべき課題

ぼく「私はオキニ嬢が決まっている。その出勤を確認したいだけなんだ・・・・。」
でも電車では風俗サイトなんて見れない、自宅でも家族がいるし、嫁は相手にしてくれない、でも独りの時間にそんなサイトばっかり見て時間が過ぎ去るのは嫌だ!

私が必要とするサービスを自分のために作ってみました。

1:見る場所問題
風俗関係のウェブサイトってどうしても見る場所を選びますよね。じっくり新人や新店舗を見たい人に関しては、専用サイトで見る必要がありますしそれでいいと思います。しかし、すでにお気に入り嬢が決まっていてその娘達の出勤だけ見たいという人には、XXXサイトを行き来するのは大変な苦労です。

当然家族団欒の場や電車などの公共の場では見る事ができません。

2:時間の浪費問題
私はお気に入り嬢の出勤だけを見たいだけだったのに、気がつけばエロサイトをネットサーフィンしてしまい、大変な時間を費やしたことがあります。サクッと出勤だけ確認できれば他の事にも時間が使えると思いました。

3:見た目の問題
掲載されているイメージがやはりセクシーすぎて、1の問題を引き起こします。もちろん1人で見る場合にはいいのですが、公共の場には向きません。

以上の課題を解決したいと思います。

2-1 設計案

・ どこで見てても良いUI
・ 正確なお気に入り嬢の出勤情報を横断的に捕捉

簡単ですがひとまずこれを目標にして制作しました。

2-2 使用技術と骨子

・正確な嬢の出勤情報は大手風俗情報サイトよるとも[Yorutomoさん]からスクレイピングして、データを解析する事にしました。

・フォームにyorutomoの嬢の個別ページを入力するだけで、自分だけのオキニ嬢出勤表が作れるようにしました。

・ 使用技術
Ruby on rails / Heroku / nokogiri / mechanize / Boot strap /devide

3 使い方

・1:ビジネス管理アプリ風Uiにログインします。Deviseで管理画面を作っています。今のところ、confirmableは切っていますので、適当なメールアドレスでも登録できるでしょう。

スクリーンショット 2019-12-05 9.41.04.png

2:上部メニュービジネスメンバーの追加からオキニ嬢のURLをよるともからコピペします。

スクリーンショット 2019-12-05 9.32.21.png

スクリーンショット 2019-12-05 9.44.04.png

よるとも側のページは個別の嬢の詳細ページでURLは以下のような感じのはずです。
https://www.yorutomo.net/tokyo/m2/a6/shop"店番号"/gal"女性番号"/

3:いくつか自分のお気に入り嬢を登録すると店舗横断型のスケジュール表が完成します。
スクリーンショット 2019-12-05 9.32.21.png

ユーザーインターフェイスは極力風俗関係の色を排除し、嬢の名前も初めの一文字のみの表示としています。なお迷いましたが、やはり予約はこのまましたいので電話番号は表示するようにしました。

「ペン」アイコンで編集・「ゴミ箱アイコンで」削除が可能です。

4 良かった事と課題

・ 電車でも人目を気にせずお気に入り嬢の出勤を確認できるようになった。
・ 公式サイト、風俗情報サイトをサーフィンしまくる時間が削減できた。

[課題]
・ スマホにしっかり対応するUl設計。
・ 嬢の名前をはっきりと出すとステルス効果が薄れるので、現在は一文字表示にしているが良い方法はないものか。

もし需要があればURL公開しますので使ってみてください。技術としては今あるものを重ね合わせた一般的なもので作っています。

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

SlackBotにGIFを検索して流す機能を実装したら治安が崩壊した話

Qiitaご利用の諸君はおそらく社内でのコミュニケーションにSlackを利用していることだろう。

  • 特に開発しなくても便利に使える便利なアプリが山程ある
  • GitHubやGitLabの更新をメッセージとして流したりIssueなどをその場で発行できる
  • Emoji無限(?)に増やせるたのしい!!!

これだけの利点があるのだから今更強烈な事情がない限りChatOps基盤にSlackを導入するのはほぼ必然と言い切ってもいいだろう。

そしてSlackではもっとコアな機能を持ったボットなどを開発する人たち向けにAPIを用意している。

今回はSlackのAPIの一つ、RealTimeMessageAPIを用いて開発していたSlackのBotにとある機能を実装したこと、リリースをした話をしようと思う。

新機能の概要

Botがinviteされているチャンネルで

特定の書式(今回は put_gif )+検索文字列

と飛ばすとBotがそれを識別して適当なAPIからGIF画像のリンクを引っ張り、それを投げるというような機能を実装しようという話がチャンネル内で上がった。

丁度卒業研究が終わったところで暇だったので片手間で実装、そのままリリースすることにした。

実装の概略

GIF検索に利用するAPI

今回はtenor APIの提供するAPIを使う。
似たようなAPIにGIPHYがあるが、使ってみるとあまり検索結果がよくなかった(設定が悪かった?)ので移行した。

利用する言語など

アプリケーション全体の実装をRuby(v2.6.5)で行った。
また、RTM APIとの相互通信のためにWebsocketを使うため、faye/websocketも利用させてもらっている。

デプロイ

個人で持っているEC2インスタンスの中にdocker及びdocker-composeを使って置いた。

GIFを拾ってくる部分の関数に関して

機能のコア部分だけ切り抜いてきた。
これをメソッドとして入れてやればGIFのリンクを返す関数ができる

gif_get.rb
# frozen-string-literal: true

def gif_get(search_query)
  require 'http'
  # 環境変数は`docker-compose.yml`経由で`.env`を参照している
  tenor_api_key = ENV['TENOR_API_KEY']
  response = JSON.parse(HTTP.get('https://api.tenor.com/v1/search',
                                 params: { key: tenor_api_key,
                                           q: search_query,
                                           limit: 50 }))

  # 検索結果が0の場合は'SearchCount: 0`とだけ返して、
  # それ意外の場合は `Array#sample` でランダム抽出してURLを返す
  if response['results'].empty?
    'SearchCount: 0'
  else
    response['results'].sample['media'][0]['gif']['url']
  end
end


実際の成果物

output.gif

ローカルでコンテナを展開しているので少し応答が遅れてしまっているが、本番のEC2上においているコンテナは1秒以内で応答を返してくれるので常用可能な機能になっている。


ここからが本題

で、弊部のワークスペースに入れた結果


なんだよこれ

TAGは関係ねえぞ

THE 世紀末と言わんばかりに遊ばれてしまった。事実今日だけで300くらいはメッセージの送受信があったように思える

この機能を一般的なグループが入れて良いものか

結論から言うと進捗阻害にしかならないのでやめておいたほうがいい(あたりまえ)

今回は非公認のサークル組織であるためにこんなもんがまかり通っているが、汎用性はともかくとして実はこれ以外に 色々モラル的にアレなやつまで出てきてしまった ことがあった。

他の方々などはやるのであればAPIアクセス時のリクエストパラメータに contentfilter というキー項目があったのでそちらを meddium なり high なりに指定してフィルタリングをかけたほうが良い。

逆にSlackにセンシティブなもんが出したい!みたいな人は off でもいいんじゃないだろうか。覗き見られたら死亡だけど。

最後に

開発しているSlackBotはソースコードをGitHubにて公開しているのでSlackを面白い感じにしたいサークルなどがあれば触っていただければ、と思います
nkc-ug/NKCUG_Slack_Bot

PRや新機能導入のIssueなどは弊部部員以外の方も歓迎します。
ただし導入して進捗が落ちたとかの文句は受けないぞ。

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

RailsエンジニアがReactNativeエンジニアになるにあたって苦しんだところワースト3

top.png

はじめに

こんにちは、株式会社OKANで「おかんPay」というプロダクトのPdMをしながらも、自らバリバリReact Nativeで実装している かなすぎ と申します。
元々は、RailsでOKANの社内システムを構築していたサーバーサイドエンジニアだったのですが、志願してReact Nativeを使うアプリエンジニアになってから約半年が立ちました。
そこで、RailsエンジニアがReact Nativeを扱うアプリエンジニアになる時に苦しんだ箇所をまとめることで、自分と同じようにReact Nativeを新たに勉強する人の助けになればと思いながら記事を書きました。

想定読者

  • Railsエンジニアだが、これからReactNativeアプリもつくってみたいなって思っている人
  • React.js, Vue.jsなどフロントエンドフレームワークをがっつり実装したことないけど、React Nativeでアプリ作ってみたい人

RailsエンジニアがReactNativeエンジニアになるにあたって苦しんだところワースト3

RailsではJavascriptも使うことはあったのですが、MVCでいうViewのDOM操作をjQueryで軽く行うくらいの経験値であったので、ES6でがっつりロジックを書くというのも覚えることが多くて大変でした。また、Vue.jsやReact.jsなどで用いられる状態管理や、React.js特有のライフサイクルメソッドやJSX。そして、ウェブ開発では存在しないアプリ特有のDeepLinkや画面遷移の方法なども大きく異なっているので、キャッチアップに苦労しました。

ワースト1位:ES6(ES2015)

React NativeはES6で書きます。Javascriptは、ES6以降とそれより前では使える機能は大きく変わりました。現在流行りのフロントエンドのフレームワークであるAngular.js、React.js、Vue.jsなどでも、基本的にはES6以降の書き方で書くことが多いです。JavascriptはViewの見た目を動的に変える軽量な言語という認識でしたが、ES6を勉強してからは、認識が大きく変わりました。

イマドキのJavaScriptの書き方2018 - Qiita
ES2015(ES6) 入門 - Qiita

アロー関数

ES6以前では、関数を定義する時は、functionを使用していましたが、ES6以降では、アロー関数を用います。記号ばっかりで最初は慣れないかと思いますが、慣れれば、短い記述で関数を定義できるので非常に便利です。()を省略できたり、様々な記述パターンがあるので、引数の数など場合によって使いわけましょう。

// 何もしない関数
() => {}

// 引数が一つだったら「()」不要
value => {}

// ブロックないが1行しかないときは、「{}」不要
(a, b) => a + b

// オブジェクトは「()」が必要
(a, b) => ({ a: b, b: a })

// 複数行はいつも通り
(a, b) => {
  const c = a * 100;
  return {
    c,
    d: b / 100,
  };
}

アロー関数
JavaScript アロー関数を説明するよ - Qiita
アロー関数を使って効率的にJavaScriptを記述する

import / export

モジュール毎に機能を管理する時に使います。モジュールで機能を管理することによって、他コードとの依存性を少なくして保守性を高めたり、変数名の競合を防いだり、汎用性の高いモジュールをつくって同じコードを繰り返すのを防いだりできます。

import React from 'react';
import {
  View,
  Text,
  StyleSheet,
  FlatList,
  Image,
  TouchableHighlight,
  Alert,
  Linking,
} from 'react-native';

export default ItemList;
export const calories;
export function pay(){...}
export class Payment {...}

import
export
ES6のexportについて詳しく調べた - Qiita

非同期処理 async / await

hidouki.js
// 非同期のpayメソッド
const pay = async (amount) => {
  const items = await getItems();
  const totalAmount = amount + tax;
  try {
    await this._payByCreditcard(totalAmount, items);
    return true;
  catch(e) {
    console.warn(e);
    return false;
  } 
};

非同期処理には、かなり苦しめられました。Rubyは同期処理できる言語なので、コードを書いたら上から順に実行されます。しかし、Javascriptは非同期であり、たとえばAPIをコールしても、その結果を待たずに、次の行のコードの処理を進めることができてしまうのです。そこで、Promiseオブジェクトを用いて非同期処理を行い、asyncで非同期関数を定義し、awaitで非同期の結果が帰ってくるまで待つことができるのです。

JavaScriptの同期、非同期、コールバック、プロミス辺りを整理してみる - Qiita
【JavaScript入門】誰でも分かるPromiseの使い方とサンプル例まとめ! | 侍エンジニア塾ブログ(Samurai Blog) - プログラミング入門者向けサイト
async/await 入門(JavaScript) - Qiita

ワースト2位:React

React NativeではReactを使ってAndroidアプリとiOSアプリを作ることができます。したがって、React自体の状態管理であったり、JSXなどReact独特の考え方・概念をキャッチアップする必要があります。フロントでReactを使用したことがあれば問題ないのですが、RailsをAPIモードで使用していた経験もなく、SlimでViewを書いていたので、Reactの概念を理解するのも大変でした。

React Native · A framework for building native apps using React
React - ユーザインターフェース構築のための JavaScript ライブラリ

Component

ReactはComponent志向で作られます。Railsエンジニアだと、Componentとは?というところのキャッチアップから始める必要があります。Componetをざっくり説明すると、UIを独立させて再利用できるようにして、propsという任意の値を親要素から受け取り画面上に表示させるものです。この文章だけ読んでもわけが分からないと思うので、サンプルコードなどを見ながら理解すると、理解が深まると思います。

// ■ Reactのコンポーネントたち

// 【Component】
// Stateを持っていて、再レンダリングの条件を自分でチューニングしたいときに使う。
class FooComponent extends React.Component {
  render() {
    return <View />;
  }
}

// 【PureComponent】
// Stateを持っていて、再レンダリングの条件をチューニングしないときに使う。
class BarComponent extends React.PureComponent {
  render() {
    return <View />;
  }
}

// 【Functional Component】
// Stateを持たなくて良い場合に使う。
const FuncComponent = props => {
  return <View />;
};

フロントエンドのコンポーネント設計に立ち向かう - Qiita

状態管理(state)

stateとは、Componentの状態を管理するデータです。たとえば、isPayableというフラグがたっていたら支払いのロジックを動かすということをDBでフラグを管理しなくても、フロント側で管理することができるのです。Reactでは、eduxという状態管理のライブラリを使用することが多いのですが、弊社のアプリは軽量なアプリなのでunstatedというライブラリを用いて状態管理をしています。個人的には、Reduxは冗長になってしまうので、unstatedの方が直感的でわかりやすくオススメです。

[React Native] 基本を学ぶ - Stateでコンポーネントの状態を管理する | Developers.IO
コンポーネントの state - React
jamiebuilds/unstated
次の状態管理はReduxをやめてunstatedにする理由 - Qiita

props

Componentは、propsという任意の引数(正確には引数ではありません)を受け取って、値を当てはめたReact要素を返します。初めてReactを勉強した時は、stateとpropsをごっちゃにしてしまい、それぞれの役割を正確に理解できませんでしたで苦労ポイントです。

[React Native] 基本を学ぶ - Propsでコンポーネントのパラメータを設定する | Developers.IO
コンポーネントと props - React

JSX

JSXはReactのComponent内でのXML風の構文です。React.jsでは、HTMLでお馴染みの<h1>タグなどが利用できるのですが、React Nativeでは、<View><Text>といった見慣れないタグを扱います。さらに、アプリエンジニアの方は理解しやすいそうなのですが、とかとかアプリ独特のComponentを使用する必要があるので、実際に動かしてみれば、「あーこれか。アプリで使ったことある」と思うのですが、Componentの名前などを覚えるのには一苦労しました。

ActivityIndicator · React Native

class Payment extends React.Component {
  // ここがstateで状態をComponent内で管理している
  state = {
    isPayable: true,
    items: {},
    totalAmount: 0,
  };

  _onPressPayment = () => {
    { isPayable } = this.state;
    if (isPayable) {
      const result = await pay(amount);
      if (result) {
        this.setState({ isPaylable: false});
      }
    }  
  }

  render() {
    return (
      // <Text>タグや<TouchableHighLight>がJSX
    // onPress={this._onPressPayment}がprops
      <TouchableHighlight onPress={this._onPressPayment}>
        <Text>支払いをする</Text>
      </TouchableHighlight>
    )
  };
}

ライフサイクルメソッド

ここが個人的には一番ReactNativeエンジニアになって辛かったです。未だに、あれこれってComponentDidMountであってるよね?って確認したくなります。Reactでは複数回renderメソッドが走るので、どのタイミングではどんなデータを用意してなければならないといったことも考慮しなくてはならないので、奥が深いです。さらにReactHooksでの書き換えも「おかんPay」では行っているので、useEffectなどの使い方は悪戦苦闘しています。

lifecycle.js
// 色々なライフサイクルメソッドがあります。
componentDidMount()
componentDidUpdate(prevProps, prevState, snapshot)
componentWillUnmount()

【React Native】React Native ライフサイクルメソッドについて|Fresopiya
React component ライフサイクル図 - Qiita

ワースト3位:ReactNativeアプリやアプリ開発特有の概念

Web開発では存在しない概念であったり、Web開発の場合と概念が異なることがあり、Railsエンジニアの方にとっては、新しく勉強することが多いです。そこで、自分がReactNativeエンジニアになるにあたって特に苦労したことをまとめました。

React Navigationの画面遷移

画面遷移については、Railsでもある概念です。Routes.rbでルーティングを設定して、redirectやrenderを用いて画面遷移を実現します。しかし、ReactNativeなどアプリの開発では、画面を重ねていく、つまりStackしていくといったイメージの方がしっくりくると思います。RailsでWeb開発だけをしていた自分にとっては、なかなか理解するのに時間がかかるところでした。ライブラリによっても仕様は異なるのですが、弊社ではReact Navigationを使用しています。

okanpay_navigation.gif

React Navigation · Routing and navigation for your React Native apps
React Navigation 3.x チートシートつくってみた - Qiita

DeepLinkとかLinking

アプリ同士の連携をするのには、DeepLinkやLinkingというモジュールをReact Native開発では使います。アプリエンジニアにとってはお馴染みの概念なようなのですが、Railsエンジニアの自分にとっては、未知の世界でした。urlを叩いたりするとアプリを開いたりする機能はこうやって実現するのかと素直に感心した領域です。日本語のドキュメントが少ないので、調べるのも英語だったので一苦労でした。今回紹介するのは、React NavigationのDeepLinkになります。

Deep linking · React Navigation
Linking · React Native
ディープリンクをめぐる歴史とReact NativeにFirebase Dynamic Linksを導入する手順 - KitchHike Tech Blog

アプリのリリース

iOSとAndroid共にリリースを何度も経験をしているのですが、慣れれば作業なのですが、一番初めのリリースはこんなにも時間がかかるものなのかと驚愕しました。また、弊社プロダクトの「おかんPay」のReactNative版を初リリースしようとしたタイミングでAndroidの審査も始まったらしいというタイミングだったので、てんやわんやでした。さらに、Google Play ConsoleのUIや仕様はかなり頻繁に更新されてしまうので、Qiitaの記事なども最新記事を追わないないと、「そのボタンどこにあるの?」みたいなことは多発しました。初めてのアプリリリースには、余裕をもったスケジュールにすることをオススメします。

iPhoneアプリ申請やAppleの審査に関するメモ - Qiita
Androidアプリにも審査がされるようになった件 - Qiita
React NativeでiOS, Androidのストア公開のTips - Qiita

iOSとAndroidのレイアウト差分

iOSとAndroidでは、PickerやCalendarで日付を選択する時など、多くの場合で、レイアウトが異なります。ReactNativeたった1つの言語でiOSとAndroidの両方のアプリを作れることは非常に大きなメリットではあるのですが、両者を全く同じに扱って言い訳ではなく、レイアウトをPlatformによって変更する必要があります。

また、エミュレータと実機の差分もあったり、Androidでは、HuaweiやSonyといった機種依存でレイアウトが変わったり、Androidのバージョンによってもレイアウトが崩れたりします。したがって、Androidすべての機種で実機テストをすることはできないので、レンタルサービスなどを用いてテストしたりしています。

import {Platform, StyleSheet} from 'react-native';

const styles = StyleSheet.create({
  width: Platform.OS === 'ios' ? 200 : 100,
});

【React-Native】iOS, Android両対応する。 - Qiita
配備機種一覧 - 実機検証用スマホ・モバイルデバイスレンタル|テスト受託サービスのケータイラボ

最後に

いかがだったでしょうか?Railsエンジニアには馴染みのない概念が多かったと思います。
この記事だけでは、もちろん十分ではないものの、自分がここ半年で苦労した箇所を中心にまとめてあるので、何かの助けになれば幸いです。

次は、SocialDogのアプリ開発している@t0m0120さんです。
私もSocialDogは、Twitterを活用するのに利用させてもらっています。どんな記事が読めるのか楽しみです!

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

【Rails】bundle installをしたら大量のファイルを作る問題

初めに

bundle installをした際にgithubDesktopに新しいファイル生成10000ファイルのようにたくさんのファイルを生成したことはあるでしょうか。この問題に時間を使ってしまったので書き残します。

結論

bundle installをした際に大量にファイルが生成されるのは問題ではないです。正常な挙動です。本来、railsのgemのシステムは大量のファイルの上で成り立っておりそのファイルの数を普段見ることはないですがgithubDesktopでその大量の変更を見ることができるのが問題と言えるでしょう。

原因

私は普段はgem fileにgemを追加した後にbundle installとターミナルに打つことでinstallをしているのですが、qiitaの記事を真似していてbundle install --path vendor/bundleと入力しました。この後に続く表記はbundle installしたファイルをどこに保存するのかということを指定しているもので本来は毎回このように打つことが正しいようです。ただしこのように打ったことでgithubの管理下に置かれてしまったらしく、省略してbundle installをしても大量のファイルがgithubDesktop上に表示されるようになりました。

解決

.bundlevendor/を一度ディレクトリから消すことで解決できます。
もう一度bundle installをしたら元の挙動に戻ると思います。

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

あ!こんな引数とれたんだっていう Ruby のメソッド 8選

ENECHANGE で勤務している cuzic です。

これは Ruby Advent Calendar 2019 の 5日目の記事です。

今日は Ruby のメソッドの中で、「あ、こんな引数とることができたんだ」と
個人的に感じたメソッドをいくつか紹介します。

self.class::CONSTANT

実は ::演算子の左辺には式を指定できます。

下記のようなコードがあったとします。

class A
  NUMBER = 1
  def initialize
    @number = A::NUMBER
  end
end

これは下記のように書くことができます。

class A
  NUMBER = 1
  def initialize
    @number = self.class::NUMBER
  end
end

このように :: 演算子の左辺として self.class のように式を指定することができます。

上記の例であれば

a = A.new
p a.class::NUMBER #=> 1

のように self.class 以外も書くことができます。

array.values_at(range1, range2)

引数で指定されたインデックスの要素を配列として返すメソッド Array#values_at ですが、
Range を複数個、引数としてとることができます。

array = ('a'..'z').to_a
array.values_at(0..2, 10..12)
=> ["a", "b", "c", "k", "l", "m"]

ENECHANGE ではさまざまな電気料金プランの料金計算を行っているのですが、
中には季節によって電気代が違うものの、春と秋は同じ単価という電気料金プランがあります。
春と秋の電気使用量を取り出すとき、このように range を複数引数として与えて
values_at メソッドを使うと簡潔になります。

string[regex, index]

文字列の [] メソッドは第1引数に正規表現、第2引数に index を指定することで、
正規表現の capture を簡単に取り出すことができます。

str = "Hello, World!"
str[/(.)\1/] #=> "ll"
str[/(.)\1/, 0] #=> "ll"
str[/(.)\1/, 1] #=> l

region = "ap-northeast-1a"
region[/^(.+).$/, 1] #=> "ap-northeast-1"

time.to_s(:db)

Ruby on Rails に含まれる Active Supportの中では
Time#to_s が拡張されており、さまざまな引数を
とることができます。

例を紹介します。

time = Time.current
time.to_s(:db)
=> "2019-12-03 13:17:17"

time.to_s
=> "2019-12-03 22:17:17 +0900"

time.to_s(:time)
=> "22:17"

time.to_s(:number)
=> "20191203221717"

time.to_s(:short)
=> "03 Dec 22:17"

time.to_s(:long)
=> "December 03, 2019 22:17"

time.to_s(:long_ordinal)
=> "December 3rd, 2019 22:17"

time.to_s(:rfc822)
=> "Tue, 03 Dec 2019 22:17:17 +0900"

time.to_s(:iso8601)
=> "2019-12-03T22:17:17+09:00"

time.to_s(:nsec)
=> "20191203221717054977424"

time.to_s(:usec)
=> "20191203221717054977"

特に :db が便利です。

他には、nsec はわりと使いどころがあるわりに、あまり知られていない気がします。

Array#uniq{|item| ... }

Array#uniq はブロックを引数としてとることができます。

そのブロックを評価した値が重複しないような配列を取得することができます。

[*2..5, *"1".."4"].uniq(&:to_i)
=> [2, 3, 4, 5, "1"]

enumerable.first(n), enumerable.last(n)

Enumerable モジュールの first や last メソッドは引数として
取り出す個数をとることができます。

range = "a".."z"

range.first(3) #=> ["a", "b", "c"]

range.last(3) #=> ["x", "y", "z"]

postcode = "1300012"
postcode_1 = postcode.slice(0, 3) #=> "130"
postcode_1 = postcode[0, 3] #=> "130"
postcode_1 = postcode[0..2] #=> "130"
postcode_1 = postcode[0...3] #=> "130"
postcode_1 = postcode.first(3) #=> "130"

enumerable.to_h{|item| [item["key"], item["value"]}

Enumerable モジュールの to_h メソッドは引数としてブロックを
とることができます。(Ruby 2.6 以降)

例えば、下記のようなコードを実行できます。

 (1..9).to_h{|i| ["%02d" % i, i]}
=> {"01"=>1, "02"=>2, "03"=>3, "04"=>4, "05"=>5, "06"=>6, "07"=>7, "08"=>8, "09"=>9}

これまで、

 (1..9).map{|i| ["%02d" % i, i]}.to_h

とすることで、同様の効果がありましたが、より簡潔かつ効率よくハッシュを生成することかできます。

Array.new(n){|index| ... }

Array.new は配列の数を引数にとれるだけでなく、ブロックを引数としてとることができます。

例えば、下記は ENECHANGE で実際に使われていた month_from から month_to までで指定される
季節に該当するかどうかを判定するメソッドです。

    def in_season?(month_from, month_to, month)
      month_from = ( month_from || 1 ) - 1
      month_to = ( month_to   || 12 ) - 1
      width = (month_to + 12 - month_from) % 12 + 1
      months = Array.new(width) do |i|
        (month_from + i) % 12 + 1
      end

      months.include?(month)
    end

その季節に含まれる月の配列(months)を生成する処理のところで、 Array.new にブロックを引数として
与えています。

おわりに

今回は Ruby のメソッドの中で比較的、そんな引数をとることができることが知られていないようなものを
紹介しました。

Rubyリファレンスマニュアルを読んでいると、いろいろと今まで分かっていたつもりで分かっていなかった
様々な発見があります。

一度は通読してみること、オススメです。

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

あ!こんな引数とれたんだっていう Ruby のメソッド 8選いた

ENECHANGE で勤務している cuzic です。

これは Ruby Advent Calendar 2019 の 5日目の記事です。

今日は Ruby のメソッドの中で、「あ、こんな引数とることができたんだ」と
個人的に感じたメソッドをいくつか紹介します。

self.class::CONSTANT

実は ::演算子の左辺には式を指定できます。

下記のようなコードがあったとします。

class A
  NUMBER = 1
  def initialize
    @number = A::NUMBER
  end
end

これは下記のように書くことができます。

class A
  NUMBER = 1
  def initialize
    @number = self.class::NUMBER
  end
end

このように :: 演算子の左辺として self.class のように式を指定することができます。

上記の例であれば

a = A.new
p a.class::NUMBER #=> 1

のように self.class 以外も書くことができます。

array.values_at(range1, range2)

引数で指定されたインデックスの要素を配列として返すメソッド Array#values_at ですが、
Range を複数個、引数としてとることができます。

array = ('a'..'z').to_a
array.values_at(0..2, 10..12)
=> ["a", "b", "c", "k", "l", "m"]

ENECHANGE ではさまざまな電気料金プランの料金計算を行っているのですが、
中には季節によって電気代が違うものの、春と秋は同じ単価という電気料金プランがあります。
春と秋の電気使用量を取り出すとき、このように range を複数引数として与えて
values_at メソッドを使うと簡潔になります。

string[regex, index]

文字列の [] メソッドは第1引数に正規表現、第2引数に index を指定することで、
正規表現の capture を簡単に取り出すことができます。

str = "Hello, World!"
str[/(.)\1/] #=> "ll"
str[/(.)\1/, 0] #=> "ll"
str[/(.)\1/, 1] #=> l

region = "ap-northeast-1a"
region[/^(.+).$/, 1] #=> "ap-northeast-1"

time.to_s(:db)

Ruby on Rails に含まれる Active Supportの中では
Time#to_s が拡張されており、さまざまな引数を
とることができます。

例を紹介します。

time = Time.current
time.to_s(:db)
=> "2019-12-03 13:17:17"

time.to_s
=> "2019-12-03 22:17:17 +0900"

time.to_s(:time)
=> "22:17"

time.to_s(:number)
=> "20191203221717"

time.to_s(:short)
=> "03 Dec 22:17"

time.to_s(:long)
=> "December 03, 2019 22:17"

time.to_s(:long_ordinal)
=> "December 3rd, 2019 22:17"

time.to_s(:rfc822)
=> "Tue, 03 Dec 2019 22:17:17 +0900"

time.to_s(:iso8601)
=> "2019-12-03T22:17:17+09:00"

time.to_s(:nsec)
=> "20191203221717054977424"

time.to_s(:usec)
=> "20191203221717054977"

特に :db が便利です。

他には、nsec はわりと使いどころがあるわりに、あまり知られていない気がします。

Array#uniq{|item| ... }

Array#uniq はブロックを引数としてとることができます。

そのブロックを評価した値が重複しないような配列を取得することができます。

[*2..5, *"1".."4"].uniq(&:to_i)
=> [2, 3, 4, 5, "1"]

enumerable.first(n), enumerable.last(n)

Enumerable モジュールの first や last メソッドは引数として
取り出す個数をとることができます。

range = "a".."z"

range.first(3) #=> ["a", "b", "c"]

range.last(3) #=> ["x", "y", "z"]

postcode = "1300012"
postcode_1 = postcode.slice(0, 3) #=> "130"
postcode_1 = postcode[0, 3] #=> "130"
postcode_1 = postcode[0..2] #=> "130"
postcode_1 = postcode[0...3] #=> "130"
postcode_1 = postcode.first(3) #=> "130"

enumerable.to_h{|item| [item["key"], item["value"]}

Enumerable モジュールの to_h メソッドは引数としてブロックを
とることができます。(Ruby 2.6 以降)

例えば、下記のようなコードを実行できます。

 (1..9).to_h{|i| ["%02d" % i, i]}
=> {"01"=>1, "02"=>2, "03"=>3, "04"=>4, "05"=>5, "06"=>6, "07"=>7, "08"=>8, "09"=>9}

これまで、

 (1..9).map{|i| ["%02d" % i, i]}.to_h

とすることで、同様の効果がありましたが、より簡潔かつ効率よくハッシュを生成することかできます。

Array.new(n){|index| ... }

Array.new は配列の数を引数にとれるだけでなく、ブロックを引数としてとることができます。

例えば、下記は ENECHANGE で実際に使われていた month_from から month_to までで指定される
季節に該当するかどうかを判定するメソッドです。

    def in_season?(month_from, month_to, month)
      month_from = ( month_from || 1 ) - 1
      month_to = ( month_to   || 12 ) - 1
      width = (month_to + 12 - month_from) % 12 + 1
      months = Array.new(width) do |i|
        (month_from + i) % 12 + 1
      end

      months.include?(month)
    end

12月から4月までというのが 12、1、2、3、4 を返すことができるように上記のコードは少し工夫しています。

おわりに

今回は Ruby のメソッドの中で比較的、そんな引数をとることができることが知られていないようなものを
紹介しました。

Rubyリファレンスマニュアルを読んでいると、いろいろと今まで分かっていたつもりで分かっていなかった
様々な発見があります。

一度は通読してみること、オススメです。

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