20201208のRubyに関する記事は30件です。

WindowsでWSL2(Ubuntu)にRailsを入れてgitコマンドでgithubにpushするまで

背景

WindowsのWSL2上でRails2.7.2を動かし、ブラウザでRailsが動いていることを確認し、コンソールからgitコマンドでgithubのmasterブランチにコードをpushするまでの手順です。

また、gitでのブランチの切り分けとプルリク方法、Railsのフォルダ構成を最後に載せています。

WSL 2(Windows Subsystem for Linux 2)を使えるようにする

① コントロールパネルから「プログラム」>「プログラムと機能」>「Windows の機能の有効化または無効化」を選択する。
表示されたチェックボックスにて「Linux 用 Windows サブシステム」「仮想マシンプラットフォーム」の二つにチェックを付ける。その後再起動を要求されるので再起動する。

② Windows storeからubuntuをインストールする。

③下記コマンドでWSLのバージョンを確認する。

$ wsl -l -v
  NAME      STATE           VERSION
* Ubuntu    Running         1

バージョンが1なので下記コマンドでバージョン2にする。

$ wsl --set-version Ubuntu 2

wsl --set-version Ubuntu 2で「カーネルコンポーネントの更新が必要」という旨のエラーが出た場合、
https://docs.microsoft.com/ja-jp/windows/wsl/install-win10
上記リンクからwsl_update_x64.msiをダウンロードし C:\Windows\System32\lxss\tools に移動してから実行する。
wsl_update_x64.msiの実行後に再度wsl --set-version Ubuntu 2を実行する。

wsl_update_x64.msiC:\Windows\System32\lxss\tools に移動しないと実行時にエラーが出る。
wsl_update_x64.msiは上記リンクのx64 マシン用 WSL2 Linux カーネル更新プログラム パッケージからダウンロードできるが、ダウンロードがChromeにブロックされて進まない場合がある。その際はリンクを名前を付けて保存し左下のダウンロード警告に対し継続を選択すればダウンロードできる。

RubyとRailsの環境構築

環境構築するにあたって知っておいたほうがいい事前知識は下記の記事にまとめました。
Ruby初心者が環境構築するため rbenv、RVM、bundler、gem 等の事前知識

環境構築は下記の記事通りに実施します。
WSLで作るRuby on Rails環境構築 〜VSCode Remoteを添えて〜

※上記記事の最終盤でsudo apt install nodejsした後にrails serverして「webpackerが必要」という旨のエラーが出た場合、webpackerをインストールする必要があります。
webpackerのインストールには最新のyarnが必要ですが、aptでyarnを入れても最新版でないためwebpackerを入れる際にエラーが出ます。なので下記の順番でwebpackerをインストールする必要があります。

①下記を参考にyarnをインストールする。
yarnインストールの際にエラーがでた

②webpackerをインストールする。

$ rails webpacker:install 

再びコマンドを実行し、Railsが動いていることをブラウザから確認できました。

$ rails server 

image.png

コンソールからgitコマンドでgithubのmasterブランチにpushする

①gitに自分のemailとユーザー名を設定する。
※しないとコミット時にエラーが出ます。

$ git config --global user.email メールアドレス
$ git config --global user.name ユーザー名

②下記の記事通り実施する。
【Rails入門】 Githubを導入する方法

以上となります。

蛇足

下記の記事はGithubでプルリクする方法です。
初心者向けGithubへのPullRequest方法

下記の記事はRailsのフォルダ構成です。
デフォルトのRailsフォルダ構造

最後に、参考にさせていただいた記事の投稿者の皆様、ありがとうございます。
私の記事に不備などありましたらご指摘いただけると幸いです。

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

GCPの非同期プロダクトからのリクエストを認証する

こちらはVISITS advent calendar 14日目の記事です。

GCPのいくつかのプロダクトでは、処理の後に予め登録しておいたエンドポイントをHTTPで呼び出すことができます。

時限的に処理を開始したい場合や何かのイベントの後に特定の処理を行いたい場合などに、プロダクトと独立させて処理を実行できるため色々と融通が効きます。

受ける側もHTTPさえ受けられれば通常のWebアプリケーションでも問題ないので便利なのですが、リクエストを送ってきた相手が本当にGCPのプロダクトなのか確認する必要があります。

これについては、2019年4月頃よりサービスアカウントを用いたトークン認証(OAuth, OIDC)ができるようになったようです。

いくつか上記の認証を紹介する記事はあったのですが、具体的な認証の実装をしているものがあまり見当たらなかったので今回rubyで書いてみることにしました。

執筆にあたっては以下の記事を参考にさせていただきました。
この記事だけ読んで一通り設定できるようにしたいため、いくつか内容が重複するところあるかと思いますがご容赦ください。

GCPのHTTP認証

GCPでは以下のプロダクトについては、エンドポイントを登録しておくことで後処理をHTTPで投げられるようになっています。

  • Cloud Scheduler
  • Cloud Tasks
  • Cloud Pub/Sub

上記のプロダクトからは以下のようなプロダクトに対して処理を投げることができます。

  • Cloud Run
  • Cloud Functions
  • Cloud Endpoints
  • その他任意のサーバー/サービス

GCPのプロダクトを組み合わせた場合、基本的に認証はGCP側でよしなにやってくれるため便利です。

またGCE/GAE/GKEといったHTTPを受けられるようなプロダクトや、GCP外の自前のサーバー等でも可能です。
ただし、この場合はエンドポイントを公開しているサーバー側で認証を対応する必要があります。

認証の流れ

実際に認証する際は以下のような流れになります。

  1. 認証用のサービスアカウントを作成する
  2. 受信側で認証する
    1. 認証機構を持つプロダクトの場合:サービスアカウントにIAMで関連のロールを付与
    2. 自前の場合:送られてくるidトークンを認証する
  3. 送信側のプロダクトにサービスアカウントを紐付ける

1. 認証用のサービスアカウントを作成する

まずはサービスアカウントを作成します。
予めロールを設定するプロダクトが分かっていれば、それにあったプロダクトのロールをここで設定しますが、後ほど設定も可能なので後回しでも大丈夫です。

今回は gcp-oidc-auth@{project-id}.iam.gserviceaccount.com のような名前にしました。

2. 受信側で認証する

続いて受信側を設定します。
送信側の設定をする際に受信側のendpointを指定するので、先に受信側を用意しておく必要があります。

2-1. 認証機構を持つプロダクトの場合

GCPプロダクトで認証できる場合は、さきほど作成したサービスアカウントに受信側プロダクトのロールを付与します。
今回は例としてCloud Runを取り上げます。

なおCloud Endpointsに関しては、違った手順で認証を構成することになります。

Cloud Runサービスの作成

まずはCloud Runに飛んでサービスを作成します。

スクリーンショット 2020-12-07 23.39.35.png

リージョンやサービス名は適当に決めて次へ。

cloud_run_detail.png

コンテナを指定するところは、適当なイメージを選択します。

詳細設定内にあるサービスアカウントは、あくまでCloud Runが何かGCPのAPIを叩くときに使うサービスアカウントになります。
1で作成したものは送信側に設定するものなので、ここではCloud Run用(もっと言うとCloud Runのサービスごと)のサービスアカウントを割り当てた方が良いと思われます。

cloud_run_auth.png

3つ目にHTTPのトリガーを指定しますが、ここで「認証を必要とする」を選択して、Cloud Runサービスを作成します。

IAMの設定

最後の「認証を必要とする」では、IAMにて送信側に設定するサービスアカウントに適切なロールを付与する必要があります。
ロールは受信側プロダクトに依存したものを付与する必要があります。

  • Cloud Run: Cloud Run 起動元
  • Cloud Functions: Cloud Functions 起動元

cloud_iam_with_cloud_run.png

2-2. 自前でIDトークンを認証する

自前でIDトークンを検証する場合は、トークンの中身について把握する必要があります。

サービスアカウントを紐付けると、そのGCPプロダクトからのリクエストのAuthorizationヘッダーにBearer Tokenがjwt形式で渡ってきます。

IDトークンをdecodeするとこのような形になります。
この場合1つ目のjsonがペイロード、2つ目がヘッダーになっています。

[
  {
    "aud": "https://hogehoge.com/path/to/endpoint", 
    "azp": "11.................52",
    "email": "hoge-service-account@{project-id}.iam.gserviceaccount.com",
    "email_verified": true,
    "exp": 1606186661,
    "iat": 1606183061,
    "iss": "https://accounts.google.com",
    "sub": "11.................52"
  },
  {
    "alg"=>"RS256",
    "kid"=>"dedc012d07f52aedfd5f97784e1bcbe23c19724d",
    "typ"=>"JWT"
  }
]

ペイロードについては公式の説明がありますので、より詳しくはそちらを参照ください。

キー 内容
aud jwtのaudクレーム。Cloud Schedulerの場合デフォルトで受信側endpointのURLが入る。
azp 独自のクレーム。認証された送信者のクライアントIDを指すらしいが、OAuthにおいてwebアプリとAndroidアプリなどで同じ人なのに違うIDで管理される場合などに使うらしい。今回は対象外か。
email 独自のクレーム。サービスアカウントが入ってくる。
email_verified ユーザー認証が済んでいればtrue。おそらくOAuthで一般ユーザーが送信する場合は認証済みでないケースは想定されるが、今回のサービスアカウントの場合は基本的にtrueのはず。
exp jwtのexpクレーム。ライブラリを使えば基本期限切れのチェックはやってくれる。
iat jwtのiatクレーム。
iss jwtのissクレーム。ID tokenの場合 https://accounts.google.comaccounts.google.com のどちらかになる。
sub jwtのsubクレーム。Googleのアカウント全体でアカウントを特定できる、ユニークなasciiコード列が入るとのこと。

ヘッダーについては、IDトークンでは現在のところRS256が使われているようです。

kidは署名に用いられた鍵を表しており、Googleが公開しているDiscoveryのjwks_uriから取得できる鍵リストの中から、一致するものをdecodeに用います。
この鍵リストは定期的に変わるようなので、cacheするとしても一定期間で取り直した方が良さそうです。

IDトークンの検証手順

IDトークンの検証手順についても公式で以下の5stepで説明されています。

  1. Google発行の証明書が用いられているか検証する
  2. issクレームがgoogleのもの (https://accounts.google.com または accounts.google.com) か検証する
  3. audクレームが送信側のプロダクトごとに設定される項目と一致するか検証する
  4. expクレームが有効期限内か検証する
  5. hdパラメータを設定している場合、hdクレームが正しいか検証する

この他、サービスアカウントの場合はemailクレームも想定したものか検証した方が良さそうです。

実装

rubyでやる場合は googleauth gem(v0.13.0以降)を利用すると便利です。
自前でやる場合は証明書の管理なども面倒ですが、その辺も全部やってくれます。

endpointを指定するということでサーバーが必要になるので、今回はRailsで書きました。

Railsで認証を行う場合はControllerにおいて、
ActionController::HttpAuthentication::Token::ControllerMethods をincludeすると

  • authenticate_with_http_token(自前で例外など処理する)
  • authenticate_or_request_with_http_token(失敗時の処理はお任せ)

などで簡単にtokenが取得できるようになります。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include ActionController::HttpAuthentication::Token::ControllerMethods

  before_action :authenticate!

  private

  attr_reader :oidc_token_hash

  def authenticate!
    authenticate_or_request_with_http_token do |token, _options|
      @oidc_token_hash = Google::Auth::IDTokens.verify_oidc(token, aud: request.url)

      @oidc_token_hash['email'] == ENV.fetch('GCP_SERVICE_ACCOUNT_EMAIL') # 設定値の管理はENV以外でもOK
    rescue Google::Auth::IDTokens::VerificationError => _e
      false
    end
  end

aud/iss/(azp)のクレームはgem側でやってくれるため、emailを独自にチェックするだけで済みました。

例外処理ですが、verify_oidcはトークンがおかしい場合などにVerficationError、公開鍵周りの問題でKeySourceErrorを発生させます。

前者は入力側の問題なので400系(ここではfalseを返すので401になる)として返しておき、後者は公開鍵取得に失敗した等クライアント側はどうしようもケースということで500系として検知できるようにしておきました。
この辺りの例外の取り扱いは提供するサービスのポリシーに合わせてください。

今回は例だったのでhtmlを返す形を取っていますが、通常GCPからのリクエストを処理したい場合はapi的な処理が多いと思いますので、ActionController::APIを継承しつつauthenticate_with_http_tokenで自前で処理するのも良いと思います。

3. 送信側のプロダクトにサービスアカウントを紐付ける

続いて作成したサービスアカウントを送信側プロダクトに紐付けます。
今回は例としてCloud Schedulerを取り上げます。

Cloud Schedulerの場合はAuthヘッダーでOIDCトークンを選択するところが重要です。(公式ドキュメントはこちら)

gcp_cloud_scheduler.png

ターゲットはHTTPを選択肢、URLには受信側のendpointを指定します。
またAuthヘッダーではOIDCトークンを選択し、サービスアカウントには最初に作ったアカウントを指定します。

なお、ターゲットのURLが *.googleapis.comなGoogle APIのときは、AuthヘッダーにOAuthトークンを使用するようです。

一番下にある対象の項目は後のaudクレームの値になります。
空欄の場合はターゲットのURLが入ります。

後はcronでも手動でもいいので実行し、認証が成功するかを確認します。

cloud_scheduler_job.png

Cloud Pub/SubやCloud TasksなどもHTTPターゲットの設定とサービスアカウントが設定できるので、同様の設定で大丈夫です。

おわりに

ということでGCPの非同期系プロダクトからのリクエストを認証する設定の流れについてでした。

受信側にもし認証機構をもつプロダクトを割り当てられる場合はそちらを選択した方が楽ではありますが、idトークンの認証自体もそれほど複雑ではないので、ちゃんと導入してセキュアな状態を保ちたいですね。

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

Ruby3.0で導入される型定義!TypeprofでBlockやProcを解析してみる

この記事はRuby 3.0 Advent Calendar 2020の14日目の記事です。Rubyに導入される型定義でProcがどのように書かれるのかについてまとめています。

はじめに

Ruby3.0ではコードに型定義を与えるRBSという仕組みが加わります。Rubyで動いているプロダクトの可読性や保守性を高めるためにこの型定義の導入を考えている方も多いのではないでしょうか。
RBSによってRubyのコードに型情報を与えるための手助けをしてくれるツールとして静的解釈に基づく型解析ツールであるTypeprofがあります。RBSやTypeprofの基本的な解説は色々な記事が出ているため、本記事ではドキュメントなどにあまり載っていないTypeprofを用いたBlockやProcの型推論に関してまとめました。

使用した環境は以下の通りです

ruby 3.0.0preview1 (2020-09-25 master 0096d2b895) [x86_64-darwin18]
typeprof (0.9.0)
steep (0.37.0)
rbs (0.20.1)

準備

Procを用いた簡単なコードを準備します。

# lib/app.rb

class App
  def foo(n)
    n.to_s
  end

  def boo(fn)
    fn.call
  end

  def baz(fn)
    fn.call(5)
  end

  def bar(&fn)
    fn.call(0)
  end
end

proc1 = proc { "ブロック" }
proc2 = Proc.new { |n| n.to_s }

app = App.new

p app.foo(5)
p app.boo(proc1)
p app.baz(proc2)
p app.bar { |a| a.to_s }

まず実行してみます。

$ ruby lib/app.rb
"5"
"ブロック"
"5"
"0"

問題なく実行できました。それではtypeprofで型解析を行います。

$ typeprof lib/app.rb
# Revealed types
#  lib/app.rb:24 #=> String
#  lib/app.rb:25 #=> String
#  lib/app.rb:26 #=> untyped
#  lib/app.rb:27 #=> String

# Classes
class App
  def foo: (Integer) -> String
  def boo: (^-> String) -> String
  def baz: (Proc) -> untyped
  def bar: { (Integer) -> String } -> String
end

型解析結果が出てきました。おおよその型をコードを実行することで判断してくれます、自分で一からRBSを書く手間が省けてありがたいですね。TypeprofのドキュメントによるとProcに関しては抽象化せず基本的に渡される引数と返される値を元に具体的な型を出力するようです。

Procオブジェクトは、ラムダ式(-> { ... })やブロック仮引数(&blk)で作られるクロージャです。 これらは抽象化されず、コード片と結びついた具体的な値として扱われます。 これらに渡された引数や返された値によってRBS出力されます。

Typeprocではコードを実行することで型情報の解析をするため、どのような分析結果が出るかはコードの使用例によって異なります。今回のコードではbooには引数なしでStringを返すprocを与えているため(^-> String) -> Stringと分析されていますが、例えば以下のようにこれをintを返すprocに変更すると、typeprofの分析結果は変わります。

proc1 = proc { 1 }
p app.boo(proc1)

#=> def boo: (^-> Integer) -> Integer

また、これら両方を与えると、出入力両方がユニオンタイプの型シグネチャになります。

proc1 = proc { "ブロック" }
proc2 = proc { 1 }
p app.boo(proc1)
p app.boo(proc2)

#=> def boo: (^-> (Integer | String)) -> (Integer | String)

Procやblockの型シグネチャは以下のように行います。詳細はRBSのsyntaxドキュメントを参照してください。

# Proc
^(Integer) -> String
^(?String, size: Integer) -> bool
#Block
{ (Integer) -> (Integer | String) }

booに関してはうまく入出力の型の分析ができていますが、bazに関しては入力がProcであることしか分析できていませんね。ドキュメントを調べていくと以下のような文を見つけました。

Class.newは対応されません(untypedを返します)。

これが直接の原因かは検証できていませんが、上記のbazに与えるproc2もproc関数で書いてみます。

proc2 = proc { |n| n.to_s }
p app.baz(proc2)

# => def baz: (^(Integer) -> String) -> String

今度はちゃんと型が分析されましたね。
それでは、この結果をそのままRBSファイルにしていきます。RBSファイルの拡張子は.rbsです。

# sig/app.rbs
class App
  def foo: (Integer) -> String
  def boo: (^-> String) -> String
  def baz: (Proc) -> untyped
  def bar: { (Integer) -> String } -> String
end

ではこの型情報を元に実行ファイルの型情報のチェックを行っていきます。型シグネチャを用いた静的チェックにはsteepというgemを使います。以下のコマンドでsteepのための設定ファイルを作成します。

$ steep init

生成されたSteepfileを以下のように変更します。signatureでRBSの型シグネチャファイルのあるディレクトリを、checkでチェックしたいrbファイルのディレクトリを指定します。

target :lib do
  signature "sig"
  check "lib"
end

型情報の静的チェックを行います。

$ steep check
lib/app.rb:25:10: ArgumentTypeMismatch: receiver=::App, expected=^() -> ::String, actual=::Proc (proc1)
lib/app.rb:26:10: ArgumentTypeMismatch: receiver=::App, expected=^(::Integer) -> ::String, actual=::Proc (proc2)

型チェックのエラーが出ましたね。どうやらsteepではまだProcの引数のチェックまではできず、Procとプロック型シグネチャを不一致と見なしてエラーを吐いてしまうようなので、以下のように書き換えます。

# sig/app.rbs
class App
  def foo: (Integer) -> String
  def boo: (Proc) -> String #<=ここを編集
  def baz: (Proc) -> String #<=ここを編集
  def bar: { (Integer) -> String } -> String
end

もう一度型チェックを行います。

$ steep check

今度はエラーが出ませんでした。steepがprocの引数の型までチェックしてくれるようになるといいですね。
最終的なフォルダ構成は以下のようになっています。また、使用したコードはhttps://github.com/TomeHirata/typeprof_testに上がっています。
スクリーンショット 2020-12-08 21.43.39.png

まとめ

Ruby3.0で標準となるRBSやTypeprofを用いてrubyコードの型プロファイリングを行なってみました。自動で既存のRubyコードからRBSの型定義を分析してくれるTypeprofとても便利そうですね!Typeprofでおおよその型定義を自動生成して一部手直しすることで簡単に型定義が用意できるようになりそうです。
RBSではProcとBlockはそれぞれ以下のように書きますが、本記事執筆時点ではTypeprofでClass.newをおってくれなかったり、steepでprocの型不一致エラーが起きたりしました。

# Proc
^(Integer) -> String
#Block
{ (Integer) -> (Integer | String) }

まだ、型シグネチャの書き方や解析ツールの推論部分などにわかりにくい部分はありますが、今後改善されていくと思います。皆さんもぜひこの機会に型を持ったRubyライフを始めてみてください。また、このように書くとsteepやTypeprofでProcの定義がうまくいくよと言ったTipsがあればぜひ教えてください!!

最後に

現在 estie では, JavaScript や Ruby に強いエンジニアを積極採用中です!!

不動産のデータを使ってデータプラットフォームを構築したい、分析したい、プロダクトを作ってみたいという方はぜひ!

他のブログ採用ページをご覧ください。

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

【Rails】ymlファイルの中でerbを使い、動的に値を取得してrakeタスクの引数にする

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

deviseを利用した新規登録時のユーザー情報の保存とフラッシュメッセージについて

はじめに

deviseを利用したuser周りの設定の中で、私が困ったことについて解決方法を記述しようと思います。

今回なかなか思った通りにできずに困ったことはユーザーの新規登録です。新規登録ページの作成はできましたが、そこから先で躓いてしまいました。

  • 登録したい新規ユーザーの情報が登録されない
  • 新規登録ページで情報入力後、ボタンを押してもページが変わらない
  • 新規登録ができた場合とできなかった場合の動作を変えたい
  • 新規登録時のフラッシュメッセージの表示がされない

以上を解決した方法について備忘録として記事を書きました。

Rails 5.2.4.4
Ruby 2.5.1
を使用しています。

ルーティングの設定

最初に躓いたのはユーザー情報が保存されないことです。sendボタンを押すとそのまま動かなくなり、Sequel Proを確認しても情報の保存がされていませんでした。

ルーティングとコントローラーに問題があると考えたので、まずはroutes.rbを確認します。

現状ではおそらく既にご自身で設定したトップページなどへのルーティングと、devise導入時に自動で記述されたコードで以下のようになっていると思います。

config/routes.rb
devise_for :users

root "トップページ"
resources :その他のページ

ここに追記をしていきます。
新規登録はdeviseで自動生成されたregistrationsにあたるので以下のように記述をします。新規登録には関係ありませんが、ついでにログインに必要なsessionsについても記述しておきましょう。また、resources :usersという記述も追加します。

config/routes.rb
devise_for :users, controllers: {
  registrations: 'users/registrations',
  sessions: 'users/sessions'
} 

root "トップページ"
resources :その他のページ
resources :users

コントローラーの設定

次にコントローラーを確認します。

registrations_controller.rbを開きます、こちらはdeviseで自動生成されたファイルです。
中を見ると class ~ end の中身が全てコメントアウトされているのが確認できます。こちらにコードを記述をしていきます。

今回やりたいことはユーザー新規登録なので、newとcreateを記述します。

app/controllers/uses/registrations_controller.rb
def new
  @user = User.new
end

def create
  @user = User.new(user_params)
  if @user.save
    redirect_to root_path
  else
    render :new
  end
end

private

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

createの中では複数のことをしているので注意点の記述や解説をします。

まず

app/controllers/uses/registrations_controller.rb
@user = User.new(user_params)

この(user_params)についてははprivate以下で記述しています。

app/controllers/uses/registrations_controller.rb
def user_params
  params.require(:user).permit(:email, :password, :password_confirmation)
end

このpermitの後の部分では新規登録時に必要なカラムを記述します。例えば私の場合はニックネームも登録できるようにしたかったので、以下のように記述をしました。

app/controllers/uses/registrations_controller.rb
def user_params
  params.require(:user).permit(:nickname, :email, :password, :password_confirmation)
end

ご自身の登録したい情報によって記述を変更してください。

次の記述です。

app/controllers/uses/registrations_controller.rb
if @user.save
  redirect_to root_path
else
  render :new
end

この記述は新規登録ができた場合とできなかった場合の条件分岐をしています。
こちらでは登録ができた場合はトップページへ飛び、できなかった場合にはまたユーザー新規登録ページへと戻ってくるように記述しています。

redirect_to と render は似たような動きをしますが使い分けが必要です。こちらの記事がとてもわかりやすく解説されているので、気になる方は目を通してみると勉強になると思います。
https://qiita.com/morikuma709/items/e9146465df2d8a094d78

ここまでで以下の問題が解決しました。

  • 登録したい新規ユーザーの情報が登録されない
  • 新規登録ページで情報入力後、ボタンを押してもページが変わらない
  • 新規登録ができた場合とできなかった場合の動作を変えたい

新規登録ページから情報を入力して登録ができること、登録が失敗した場合にはまた新規登録ページに戻ってくることを確認してください。問題なく行えていれば成功です。

しかし今のままでは新規登録が成功してもトップページに飛ぶだけなので、ちゃんと登録ができたのかユーザーにはとてもわかりにくい状態です。
最後にフラッシュメッセージを表示できるようにします。

フラッシュメッセージを表示する

deviseでは設定をすれば簡単にフラッシュメッセージを表示できます。
こちらの記事がフラッシュメッセージの導入方法についてわかりやすく解説してあります。
https://qiita.com/hari00702/items/4e100b9dc78d19e8e316

しかし、ユーザー新規登録時の登録完了のメッセージはその中には入っていません。
そこでregistrations_controller.rbのcreateに以下のように追記します。

app/controllers/uses/registrations_controller.rb
def create
  @user = User.new(user_params)
  if @user.save
    redirect_to root_path, notice: 'ユーザー新規登録を完了しました' #追記
  else
    render :new
  end
end

notice: の後ろの部分の記述がそのままフラッシュメッセージになるので好みのメッセージを入れてください。
新規登録ページから実際に登録を行ってみて、ページ上部にメッセージが表示されていれば成功です。

これで残りの

  • 新規登録時のフラッシュメッセージの表示がされない

についても解決できました。

参考

https://qiita.com/mmmasuke/items/7d5c47b4a40f6912adf2

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

【Rails】既存のRails6プロジェクトにDockerを導入してみる

はじめに

既存のRailsプロジェクトをDockerに乗せていきます。
初学者による記事なので間違い等ございましたらご指摘いただけると幸いです。
※本記事は開発環境のみの導入を対象としています。

参考

Dockerが何なのか全く分からない!という方はまずは下記の記事を参考にDockerを触ってみてください。
DockerをMacにインストールする
こちらの記事も手っ取り早くDockerをふんわり理解するのにオススメです。
Dockerについてなるべくわかりやすく説明する

環境

  • macOS Catalina 10.15.7
  • Ruby 2.6.5
  • Rails 6.0.3.3
  • Docker 19.03.13
  • docker-compose 1.27.4
  • MySQL 5.6.47

目次

  • 必要ファイルの作成
    • ファイル構成
  • 作成したファイルの編集
  • コンテナの起動

必要ファイルの作成

必要ファイルは以下の5つです。rails newをした際に自動生成されるファイルもあるので必要なもののみ作成していきましょう。

  • Dockerfile
  • docker-compose.yml
  • Gemfile
  • Gemfile.lock
  • database.yml

ファイル構成

既存のアプリケーション名
└── app
     ├── Dockerfile  # 作成
     ├── docker-compose.yml  # 作成
     ├── Gemfile
     ├── Gemfile.lock
     ├── config
          └──database.yml

作成したファイルの編集

Dockerfile

Dockerfile
FROM ruby:2.6.5   #自身のrubyバージョンを指定 (ruby -v)

RUN apt-get update && apt-get install -y curl apt-transport-https wget && \
    curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
    echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list

RUN apt-get update && apt-get install -y --no-install-recommends\
    nodejs \
    yarn \
    mariadb-client  \
    build-essential  \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /myproject

COPY Gemfile /myproject/Gemfile
COPY Gemfile.lock /myproject/Gemfile.lock

RUN gem install bundler
RUN bundle install
RUN yarn install --check-files

COPY . /myproject

Rails6でwebpackerが標準になったことでyarnのインストールが必要になってきます。
Docker Hubで確認するとrubyのイメージはDebian系となっているので、yarnの公式ドキュメントに記載されているOS毎のインストール方法を参考に記述していきます。
ちなみに、yarnの公式ドキュメントは日本語表示のままだとなぜかOSの選択肢の中にMacとWindowsしか表示してくれないので英語表示に切り替えて確認しましょう。

日本語表示
Screen Shot 2020-12-08 at 19.48.15.png
英語表示
Screen Shot 2020-12-08 at 19.48.30.png

docker-compose.yml

docker-compose.yml
version: '3'

services:
  db:   #データベースのコンテナ作成
    image: mysql:5.7   #自身のmysqlバージョンを指定 (mysql --version)
    command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci
    ports: 
      - '3306:3306'
    volumes:
      - mysql-data:/var/lib/mysql 
    environment:
      MYSQL_DATABASE: myapp_development   #プロジェクト名_development
      MYSQL_ROOT_PASSWORD: password
      MYSQL_USER: root
      MYSQL_PASSWORD: password

  web:   #アプリケーションのコンテナ作成
    build:
      context: .
      dockerfile: Dockerfile
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    tty: true 
    stdin_open: true
    depends_on:
      - db
    ports:
      - "3000:3000"
    volumes:
      - .:/myproject
volumes:      #dbを永続化するための記述
  mysql-data:

Gemfile

Gemfile
source 'https://rubygems.org'
gem 'rails', '~>6.0.0'

既存アプリケーションの場合この記述はすでにあるはずですが念のため確認しておきましょう。

database.yml

database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root   #docker-compose.ymlで指定したユーザー名
  password: password   #docker-compose.ymlで指定したパスワード

development:
  <<: *default
  database: myapp_development   #プロジェクト名_development
  host: db   #docker-compose.ymlでデータベースのコンテナ作成部分で指定したservice名

test:
  <<: *default
  database: myapp_test   #プロジェクト名_test
  host: db   #docker-compose.ymlでデータベースのコンテナ作成部分で指定したservice名

アプリケーションのコンテナからデータベースのコンテナに接続するための設定を記述していきます。
セキュリティ面を気にする場合はusernameとpasswordに環境変数を設定しましょう。今回は開発環境のみということもあり設定していません。

コンテナの起動

ターミナル
cd myapp
docker-compose build    #コンテナを建てる
docker-compose up -d   #コンテナ起動(-dオプションをつけることでバックグラウンドで実行)

これで localhost:3000 にアクセスするとページが表示されます◎

  • エラーが発生した場合は
ターミナル
docker-compose logs -f

でリアルタイムでログの確認ができるのでエラー原因を探して解決しましょう。

  • サーバーを停止させたいときは以下のコマンドを実行してください。
ターミナル
docker-compose down

最後に

実装に思いの外手間取ったので備忘録的に記事にしてみました。
どなたかの参考になれば幸いです。

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

formオブジェクトで複数テーブルへ値の保存

formオブジェクトを使った複数テーブルへの値の保存を1週間位試行錯誤してやっと実装できたので、記事に残しておこうと思います。
色々な方の実装方法を真似て作ったので解釈が間違っているところや、理解が不十分なところもありますが、自分はこう解釈してこの記述をしているということで書いていきます。

前提
プログラミング初学者の備忘録的な感じで書いており、間違いがある場合がありますので、ご注意ください。
解釈やそもそもの定義など間違っている部分等ありましたらご指摘していただけると幸いです。
ターミナルでのファイル生成の記述については全て省略しています。

動作環境

macOS Catalina 10.15.7
Rails 6.0.3.4
Ruby 2.6.5p114

formオブジェクトって何?

デザインパターンの1つで、1つの投稿フォームから複数のモデルに関連するデータを更新できるものです。
簡単に言うと、1つの投稿フォームから複数のテーブルへの保存の処理をするまとめ役みたいな感じです。
そのため、formオブジェクトにはform_withメソッドに対応する機能と、バリデーションを行う機能をもたせることが必要になります。
form_withで複数のテーブルに保存するデータをformオブジェクトに送り、
届いたデータに対して、各モデルのバリデーションをformオブジェクトで行った後に複数のテーブルへデータを保存すると言った流れです。

formオブジェクトを使ったフォームの実装

今回は以下2つをポイントに実装を行いました。
formオブジェクトで記事の新規投稿、更新処理ができる。
1つの入力フォームから複数のタグの新規登録、更新処理ができる。
記事の新規投稿のみに比べ、更新処理も実装するとかなり手間がかかりました。

ER図と実際の投稿フォーム

スクリーンショット_2020-12-07_19_52_31(2).png

ER図はこのような感じで、赤枠の部分をformオブジェクトで実装しました。
formオブジェクトを使い、1つの投稿フォームでarticleとtagを保存するような設計です。
具体的な動きとしては、下図のようなフォームで記事にタグを付けて投稿し、記事とタグをそれぞれのテーブルに保存します。
HabitApp.png
ArticleとTagを紐付けるために中間テーブルとして、article_tag_relationsテーブルを作っています。

モデルについて

各モデルは以下のように記述しました。
マイグレーションファイルもカラムの部分のみをモデルの下に書いています。

Articleモデル(app/models/article.rb)

class Article < ApplicationRecord
  has_many :article_tag_relations, dependent: :destroy
  has_many :tags, through: :article_tag_relations
end

マイグレーションファイル
 t.string :title,  null: false
 t.text :output,   null: false
 t.string :action
 t.integer :user_id

Tagモデル(app/models/tag.rb)

class Tag < ApplicationRecord
  has_many :article_tag_relations, dependent: :destroy
  has_many :articles, through: :article_tag_relations

  validates :tag_name, uniqueness: true
end

マイグレーションファイル
t.string :tag_name, uniquness: true

ArticleTagRelationモデル(app/models/article_tag_relation.rb)

class ArticleTagRelation < ApplicationRecord
  belongs_to :article
  belongs_to :tag
end

マイグレーションファイル
t.references :article, foreign_key: true
t.references :tag, foreign_key: true

モデルについては2つのポイントがあり

dependent: :destroy

1つは、articleが削除されたとき、tagが削除されたときには中間テーブルのarticle_tag_relationから削除された値が含まれるレコードも削除されるようにしました。

validates :tag_name, uniquness: true

もう1つは、同じ名前のtagが複数保存されないようにしています。

コントローラーについて

コントローラーは以下のように記述しました。

class ArticlesController < ApplicationController
  before_action :authenticate_user!, only: [:new, :edit, :update, :destroy]
  before_action :set_article, only: [:show, :edit]

  def index
    @articles = Article.all.order('created_at DESC')
  end

  def new
    @article_tag = ArticleTag.new
  end

  def create
    @article_tag = ArticleTag.new(article_params)
    tag_list = params[:article][:tag_name].split(',')
    if @article_tag.valid?
      @article_tag.save(tag_list)
      redirect_to articles_path
    else
      render :new
    end
  end

  def show
  end

  def edit
    @article = Article.find(params[:id])
    @article_tag = ArticleTag.new(article: @article)
  end

  def update
    @article = Article.find(params[:id])
    @article_tag = ArticleTag.new(article_params, article: @article)
    tag_list = params[:article][:tag_name].split(',')
    if @article_tag.valid?
      @article_tag.save(tag_list)
      redirect_to article_path(@article)
    else
      render :edit
    end
  end

  def destroy
    @article = Article.find(params[:id])
    redirect_to root_path if @article.destroy
  end

  private

  def article_params
    params.require(:article).permit(:title, :output, :action, :user_id, :article_id, :tag_name, :tag_id).merge(user_id: current_user.id)
  end

  def set_article
    @article = Article.find(params[:id])
  end

end

コントローラーについては特に変わったところはなく、
tag_listがformオブジェクトで値を保存するために定義したsaveメソッドに使われるくらいです。

formオブジェクトの作成

formオブジェクトはapp/formsディレクトリを作成し、その直下にarticle_tag.rbというファイル名で作成しました。

class ArticleTag
  include ActiveModel::Model
  attr_accessor :title, :output, :action, :tag_name, :user_id, :tag_id, :article_id

  with_options presence: true do
    validates :title, length: { maximum: 40 }
    validates :output, length: { maximum: 400 }
  end

  # レコードに値があるかないかでcreateかupdateかに分岐させる
  delegate :persisted?, to: :article

  def initialize(attributes = nil, article: Article.new)
    @article = article
    attributes ||= default_attributes
    super(attributes)
  end

  def save(tag_list)

    ActiveRecord::Base.transaction do
      @article.update(title: title, output: output, action: action, user_id: user_id)

      current_tags = @article.tags.pluck(:tag_name) unless @article.tags.nil?
      old_tags = current_tags - tag_list
      new_tags = tag_list - current_tags

      old_tags.each do |old_name|
        @article.tags.delete Tag.find_by(tag_name: old_name)
      end

      new_tags.each do |new_name|
        article_tag = Tag.find_or_create_by(tag_name: new_name)
        @article.tags << article_tag
        article_tag_relation = ArticleTagRelation.where(article_id: @article.id, tag_id: article_tag.id).first_or_initialize
        article_tag_relation.update(article_id: @article.id, tag_id: article_tag.id)
      end

    end
  end

  def to_model
    article
  end

  private

  attr_reader :article, :tag

  def default_attributes
    {
      title: article.title,
      output: article.output,
      action: article.action,
      tag_name: article.tags.pluck(:tag_name).join(',')
    }
  end
end

include ActiveModel::Model

ActiveModel::Modelというモジュールをincludeメソッドで与えます。
この記述によって ArticleTagクラスがモデルとしての機能を行えるようになります。
form_withへの対応やバリデーションを行うために必要な記述です。

delegateメソッド

指定したオブジェクトにメソッドの実行を委譲させるものです。
委譲:あるオブジェクトの操作を一部他のオブジェクトに代替させる手法
言葉が難しいです。。。

delegate :メソッド名, to: :委譲先のオブジェクト

ここではpersisted?メソッドをarticleというオブジェクトに委譲しています。
to_modelメソッドと合わせて、
レコードに値が存在しないときにcreateアクション
レコードに値が存在するときはupdateアクションを動かすために必要な記述になります。

to_model

モデルであるためには、to_modelを定義する必要があります。
コントローラーやview helperにモデルが渡ったときにto_modelを呼んでモデルを操作するためです。

理解が浅いためうまく説明できないのですが、delegateメソッドで値があるときと無いときに応じてPOSTやPATCHの処理を切り替え、
to_modelメソッドはアクションのURLを適切な場所に切り替えているということらしいです。

initializeメソッド

initializeメソッドはnewメソッドでインスタンスを生成する時に初期値で設定する値などを定義するメソッドです。
attributesは属性値の意味で、

attributes ||= default_attributes
super(attributes)

arrtibutesが存在すればその値を、nilであれば、default_attributesをattributesに代入するといった記述です。
superは、スーパークラスを呼び出す記述です。
initializeをここでは再定義(オーバーライド)していますが、オーバーライドする前のinitializeメソッドを引数をattributesとして呼び出しています。
単にnewメソッドでインスタンスを生成するわけではなく、
attributesが存在すればその値を使ってインスタンスを生成
存在しなければ、default_attributesを使ってインスタンスを生成するといった記述です。
createアクションとupdateアクションを値のあるなしで使い分けるための記述を1つにまとめるために、initializeの再定義を行っていると思います。
投稿のみであれば、ここの記述は必要ありません。
更新にも対応するためにレコードに保存された値を取得するために定義しています。

default_attributes

privateメソッド以下にあるdefault_attributesは投稿フォームに入力する値のdefault値を定義しました。
articleとtagを使ってdefalut値を設定するためにattr_readerでarticleとtagを読み込んでいます。
ここでは書き込む必要はなく、値として読み込むだけの処理のためattr_readerで十分になります。

saveメソッド

saveメソッドは新規投稿と更新の両方をこのメソッド1つで行うことができます。

initializeを再定義したので、newアクションでdefault_attributesの値が入ったレコードが生成されます。
フォームの入力値をarticle_paramsとして取り出して、以下のコードで入力した値に更新するという処理にして新規登録します。

@article.update(title: title, output: output, action: action, user_id: user_id)

新規登録の場合は、defalut_attributesとして全ての値がnilのレコードが生成され、フォームの入力値(article_params)で更新するといった流れです。
更新処理の場合は、保存されたレコードの値がdefault_attributesとしてあり、フォームの入力値(article_params)で更新すると言った流れです。
タグの部分の記述については後述します。

ActiveRecord::Base.transaction

トランザクションの処理を記述する時に使うものです。
トランザクションとは、分割できないワンセットの処理単位のことです。
この中に書かれた処理で途中で例外処理(エラー)があったときには途中までの処理や結果はやらなかったことにするというものです。
ここではActiveRecord::Base.transaction doからendまでの処理
つまり、articleとtagの新規登録、更新処理がトランザクションになっています。
articleとtagの新規登録や更新する時にどこかで処理が失敗した場合、途中までやっていた処理はすべてなかったことにするということです。

タグの扱いについて

タグについては複数のタグを登録、編集できる機能にしました。
tag_list(フォームに入力したタグ)
current_tags(現在保存されているタグ、更新の場合のみ使われる変数)
old_tags(現在保存されていて、そのまま残すタグ)
new_tags(新しく保存されるタグ)
の4つを配列で定義し、配列内の各要素を保存するという流れです。

tag_list = params[:article][:tag_name].split(',')

上記の記述では、フォームで送信されたparamsからタグの値を取り出します。
このときsplit(',')では入力した値を,で区切って要素に分解して配列にするといった処理が行われます。
例えばタグを入力するフォームに

朝,昼,夜

と入力した場合、

tag_list = ["朝","昼","夜"]

と入ることになります。

新規登録の場合

current_tags = @article.tags.pluck(:tag_name) unless @article.tags.nil?

unless @article.tag.nil?
となっており、タグが空でない場合にcurrent_tagsが定義されるため新規登録の場合は定義されません。
new_tags = tag_listとなり以下の処理に移ります。

new_tags.each do |new_name|
   article_tag = Tag.find_or_create_by(tag_name: new_name)
   @article.tags << article_tag
   article_tag_relation = ArticleTagRelation.where(article_id: @article.id, tag_id: article_tag.id).first_or_initialize
   article_tag_relation.update(article_id: @article.id, tag_id: article_tag.id)
end

2行目:find_or_create_byメソッドではTagモデルを通じてTagsテーブルから、tag_nameがnew_nameのものを探し、なければその値を保存します。
3行目:次の行で保存されたarticle_tagを@article.tags、つまり投稿した記事のタグの配列に格納します。
4行目:中間テーブルに値を保存する処理です
first_or_initializeメソッドは新規登録の場合はinitializeつまり新しくレコードが生成され、更新の場合はレコードは生成されません。
5行目:生成したレコード、元々あったレコードをupdateメソッドで更新する
と言った流れで新規登録されます。

更新の場合

更新の場合はtag_listとcurrent_tagsを使って、
編集された時に削除されたタグを中間テーブルから削除し、新しく追加されたタグをTagsテーブルと中間テーブルに保存する必要があります。

current_tags = @article.tags.pluck(:tag_name) unless @article.tags.nil?
old_tags = current_tags - tag_list
new_tags = tag_list - current_tags

例として、元々登録していたタグをtag1,tag2とします。
1行目:投稿した@articleに紐づくtag達を配列形式で取得します。
tagsテーブルからpluckメソッドでtag_nameというカラムを指定し、投稿した記事につけたタグのtag_nameの値を配列に格納します。
2行目:元々登録していたけれど、編集によって削除されたタグをold_tagと定義しています。
例えば、元々tag1,tag2があって編集画面でタグの欄をtag1だけにした場合はtag_listはタグのフォームに入力された値であるため、
old_tags = ["tag1", "tag2"] - ["tag1"] =["tag2"]となります。
3行目:元々登録されていなかった新規のタグをnew_tagsと定義しています。
例えば、元々tag1,tag2があって編集画面でtag1,tag2,tag3とした場合、
new_tags = ["tag1", "tag2", "tag3"] - ["tag1", "tag2"] = ["tag3"]となります。

old_tagsについての処理

old_tagsはフォームから削除され、投稿につけなくなったタグです。
投稿に紐づくタグとして以下の記述で削除する必要があります。

old_tags.each do |old_name|
  @article.tags.delete Tag.find_by(tag_name: old_name)
end
new_tagsについての処理

new_tagsは新たに追加したタグなので、新規のタグの場合はTagsテーブルに保存する必要があります。
また、投稿に紐づくタグとして新たに中間テーブルに保存する必要があります。
処理の内容については新規登録の場合の説明と全く一緒です。

まとめと感想

簡単なまとめ

formオブジェクトは1つのフォームから複数のテーブルに値を保存するために使われるデザインパターンの1つ
複数のタグを保存するにはpluckメソッドやsplitメソッドをうまく使って配列に格納し、eachメソッドを使ってそれぞれのタグに保存処理を行う

感想

delegateとto_modelメソッドについてなんとなく意味は分かった気がするが、formオブジェクトに記述して細かい部分でどう動いているのか完全には理解できていないので、もう少し理解を深める。
to_modelメソッドはActiveModel::Conversationに含まれるメソッドということで、他にもよく使っているメソッドがあるため今後勉強していく。

実装内容をすべて書いたのでものすごく長くなりました。
解釈間違い等ありましたらコメントしていただけると幸いです。
記事にわかりやすくまとめる技術も学んでいかなければ。。。

参考記事

formオブジェクトについて
https://product-development.io/posts/rails-design-pattern-form-objects
https://tomo-bb-aki0117115.hatenablog.com/entry/2020/10/29/232822

タグ付け機能
https://qiita.com/E6YOteYPzmFGfOD/items/bfffe8c3b31555acd51d

トランザクションについて
https://wa3.i-3-i.info/word142.html

to_modelについて
https://www.bokukoko.info/entry/2015/12/20/Rails_%E3%81%AE%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AB%E9%96%A2%E3%81%97%E3%81%A6

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

Ruby で解く AtCoder Educational DP Contest D ナップサック問題

はじめに

AtCoder Problems の Recommendation を利用して、過去の問題を解いています。
AtCoder さん、AtCoder Problems さん、ありがとうございます。

今回のお題

AtCoder Educational DP Contest - D - Knapsack 1
Difficulty: ---

今回のテーマ、動的計画法

以前の投稿 Ruby の Hash における keys.each と each_key の違い では、ハッシュや配列にマーク代わりとして1を代入していましたが、ナップサック問題は重さ(weight)と価値(value)の二次元になりますので、価値を代入していきます。

Array コピー

ruby.rb
n, mw = gets.split.map(&:to_i)
p = n.times.map { gets.split.map(&:to_i) }
dp = Array.new(mw + 1, 0)
p.each do |w, v|
  dp_tmp = dp.dup
  w.upto(mw) do |i|
    dp_tmp[i] = dp[i - w] + v if dp[i] < dp[i - w] + v
  end
  dp = dp_tmp
end
puts dp[mw]
insert.rb
    dp_tmp[i] = dp[i - w] + v if dp[i] < dp[i - w] + v

1の代わりに、価値を代入しています。

Array 後ろから

ruby.rb
n, mw = gets.split.map(&:to_i)
p = n.times.map { gets.split.map(&:to_i) }
dp = Array.new(mw + 1, 0)
p.each do |w, v|
  mw.downto(w) do |i|
    dp[i] = dp[i - w] + v if dp[i] < dp[i - w] + v
  end
end
puts dp[mw]

詳しい内容は、けんちょんさんの ナップサック DP を in-place 化 ここら辺を参照願います。

Array コピー Array 後ろから
コード長 (Byte) 257 221
実行時間 (ms) 852 864
メモリ (KB) 75024 15044

Hash版もトライしたのですが、うまくいかなかったです。

まとめ

  • D - Knapsack 1 を解いた
  • Ruby に詳しくなった
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby インスタンス変数を外部から操作する パート2

昨日の記事Ruby インスタンス変数を外部から操作するの続きです

attr_accessorメソッド

単純にインスタンス変数の内容を外部から読み書きする場合は、
arrt_accessorメソッドを使う

class User
  #@nameを読み書きするメソッドが自動的に定義される
  attr_accessor :name

  def initialize(name)
    @name = name
  end

  #nameメソッドや、name=メソッドを明示的に定義する必要がない
end

user = User.new('太郎')
#@nameを変更する
user.name = 次郎
user.name #=> "次郎"

複数指定することも可能

class User
  #@nameを読み書きするメソッドが自動的に定義される
  attr_accessor :name, :age

  def initialize(name)
    @name = name
    @age = age
  end

  #nameメソッドや、name=メソッドを明示的に定義する必要がない
end

user = User.new('太郎', 20)
user.name #=> '太郎'
user.age #=> 20

attr_renderメソッド

インスタンス変数の内容を読み取り専用にしたい場合は、
arrt_renderメソッドを使う

class User
  #読み取り用のメソッドだけを自動的に定義する
  attr_render :name

  def initialize(name)
    @name = name
  end
end

user = User.new('太郎')
#@nameを参照する
user.name #=> "太郎"

user.name = '次郎'
#=> NoMethodError

attr_writerメソッド

インスタンス変数の内容を書き込み専用にしたい場合は、
arrt_writerメソッドを使う

class User
  #書き込み用のメソッドだけを自動的に定義する
  attr_writer :name

  def initialize(name)
    @name = name
  end
end

user = User.new('太郎')
#@nameは変更できる
user.name = "次郎"

#@nameの参照はできない
user.name
#=> NoMethodError

参考記事、書籍

チェリー本

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

CentOS 8にRuby 2.7をインストール(AppStream)

はじめに

Application Stream(AppStream)を利用してCentOS8にRuby 2.7をインストール
参考:RHEL8のパッケージ構成 - BaseOSとApplication Stream - 赤帽エンジニアブログ
   第4章 新機能 Red Hat Enterprise Linux 8 | Red Hat Customer Portal

サポート

本手法で導入した場合、Red Hat Enterprise Linux 8 Application Streams Life Cycle - Red Hat Customer Portalより、2023-05がEOLだと思われる。
それ以降に報告された脆弱性や不具合への対応は実施されない可能性がある。

LOG

インストール

# cat /etc/redhat-release
CentOS Linux release 8.3.2011

# yum module install ruby:2.7
... 略

各種確認

# which ruby
/usr/bin/ruby

# ruby -v
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsでHTMLファイルを出力する

やりたいこと

テンプレートに沿ったHTMLファイルを出力したい!

前提条件

以下のコマンドでBooksControllerを作成し、その中にHTMLファイルを出力するアクションを作成する。

  rails g controller books

テンプレートファイルを作成する

今回は以下のディレクトリを作成し、その中にテンプレートファイルを保存する。
app/views/books/template

html_template.html.erb
<!DOCTYPE html>
  <head>
    <meta charset="UTF-8">
    <title>本の詳細</title>
  </head>
  <body>
    <table>
      <tr>
        <th>タイトル</th>
        <td><%= @title %></td>
      </tr>
    </table>
  </body>
</html>

アクションを作成する

以下のようなアクションを作成する。

books_controller.rb
  def htmlfile_download
    @title = "本の題名"

    # 指定したファイルの中身を文字列で返す
    # layoutオプションの値をfalseにしておくと、レイアウトが適用されていない状態で取得できる
    template = render_to_string('books/template/html_template', layout: false)

    # HTMLファイルを生成
    send_data(template, filename: "ファイル名.html")
  end

以上です。

おまけ

render_to_stringで取ってきた値は以下のようになっています。

<!DOCTYPE html>
  <head>
    <meta charset="UTF-8">
    <title>本の詳細</title>
  </head>
  <body>
    <table>
      <tr>
        <th>タイトル</th>
        <td>本の題名</td>
      </tr>
    </table>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Active Storageで動画をアップする!!

Active Storageで動画をアップする!!

Active Storageを使用。動画をアップロードし、validatesで形式を指定する!!

お師匠方初めまして!
Active Storageはかなり便利で画像の際かなり使い勝手が良いですよね。
今回は動画にチャレンジでバリデーションまで設定しました。
他にも皆さんのオススメなどあったら聞きたいです^^

https://gyazo.com/ab51ad2729703e1b77f831205fef7550

該当するソースコード

#app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy
  has_one_attached :video
  with_options presence: true do
    validates :title
    validates :price, format: { with: /\A[-]?[0-9]+(\.[0-9]+)?\z/}
    validates_inclusion_of :price, in: 500..5000
    validates :video
  end
  validate :video_type

  private

  def video_type
    if !video.blob.content_type.in?(%('video/quicktime video/quicktime'))
        errors.add(:video, '動画は携帯で撮影したmov形式でアップロードしてください')
    end
  end
end
#app/views/posts/index.html.erb
class PostsController < ApplicationController
#省略
<video src=<%= rails_blob_path(post.video) %> type="video/mov", controls></video>

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

Active Storageで動画をアップ

★Active Storageは画像、動画が投稿できる

ab51ad2729703e1b77f831205fef7550.png

❶バリデーションに形式を記述

#app/models/post.rb
class Post < ApplicationRecord
#省略
  validate :video_type

  private

  def video_type
    if !video.blob.content_type.in?(%('video/quicktime video/quicktime'))
        errors.add(:video, '動画は携帯で撮影したmov形式でアップロードしてください')
    end
  end
end

❷ビューファイルにvideoタグを記述

#app/views/posts/index.html.erb
class PostsController < ApplicationController
#省略
<video src=<%= rails_blob_path(post.video) %> type="video/mov", controls></video>

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

Active Storageで動画投稿

★Active Storageは画像、動画が投稿できる

ab51ad2729703e1b77f831205fef7550.png

❶バリデーションに形式を記述

#app/models/post.rb
class Post < ApplicationRecord
#省略
  validate :video_type

  private

  def video_type
    if !video.blob.content_type.in?(%('video/quicktime video/quicktime'))
        errors.add(:video, '動画は携帯で撮影したmov形式でアップロードしてください')
    end
  end
end

❷ビューファイルにvideoタグを記述

#app/views/posts/index.html.erb
class PostsController < ApplicationController
#省略
<video src=<%= rails_blob_path(post.video) %> type="video/mov", controls></video>

★Active Storageは便利…

教科書

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

rails コマンドへ独自コマンドを組み込む方法

Rake タスクって何だか変ですよね。テストも書きづらいし、できることなら書きたくないですよね。

Ruby には Thor というイケてる Gem があり、これを利用するとイケてるコマンドライン・ユーティリティを書くことができます。Thor はイケてるので、サブコマンドも書くことができます。Thor を利用する場合、コマンドライン・ユーティリティは、Thor クラスを継承したクラスとして作成するので、テストも簡単で、特別な知識は不要です。

Rake タスクではなくて、Thor を利用できたら素敵ですよね。

実は rails コマンドは Thor をすでに利用しているんです。そして、Rails エンジンの場合、Thor を使って独自コマンドを提供する標準的な方法が用意されているようですが、Rails アプリケーションの場合、標準的な方法はありません。自分でなんとかするしかありません。

以降では自分でなんとかする方法を説明します。generator のアシストは受けられないので、全て手作業でファイルを修正したり、ファイルを作成したりする必要があります。

前提

本記事で作成したサンプルは https://github.com/sunny4381/rails_command_extension に置いておきます。
このサンプルは Rails チュートリアル第14章のソースコードを元にしています。Rails チュートリアルでは UserMicropost の二つのモデルが登場しますので、本記事もこの2つのモデルを操作してみたいと思います。

Rails::Command::Base

早速、独自コマンドを作成していきます。独自コマンドは、直接 Thor を継承せずに、Rails が Thor をラップしたクラス Rails::Command::Base を提供しているので、このクラスを継承するようにします。

早速、独自コマンドを実装しましょう。以下のような main_command.rb ファイルを作成し、このファイルに独自コマンドを実装していきます。

lib/commands/main/main_command.rb
require "rails/command"

module SampleApp
  module Command
    class MainCommand < Rails::Command::Base
      namespace "sample"
      @command_name = "sample"

      def hello
        say "hello"
      end
    end
  end
end

namespace@command_name を指定して、コマンド名を明示的に指定しています。この 2 つの指定がなければ sample_app:main なんていう冗長なコマンド名になってしまいます。

そして、hello というコマンドを実装しており、bin/rails sample_app:hello と実行することを意図しています。

コマンドの実装方法の詳細については、Thor の Wiki を参照ください。

bin/rails の変更

次に独自コマンドを rails コマンドに認識させる必要があります。このため bin/rails を修正して、独自コマンドを組み込みます。次のように修正します。

bin/rails
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'

#### ↓↓↓↓↓↓↓↓追加
# 独自コマンドの組み込み
require_relative '../lib/commands/main/main_command'
#### ↑↑↑↑↑↑↑↑追加

# run rails command
require 'rails/commands'

独自コマンドを実装したファイル lib/commands/main/main_command.rbrequire_relative で読み込んでいます。独自コマンドを rails コマンドのコマンド一覧へ登録する処理が Rails::Command::Base にありますので、読み込むだけで rails コマンドに登録されます。

試しに bin/rails を実行してみます。

$ bin/rails
  ...
  routes
  runner
  sample_app:hello
  secret
  secrets:edit
  ...

多数のコマンドが出力されるので少しわかりづらいですが、rails の標準コマンドに混じって sample_app:hello と独自コマンドが表示されています。

試しに独自コマンドを実行してみます。

$ bin/rails sample_app:hello
hello

モデルの操作とサブコマンド

コンソールに文字列を表示するような単純な処理ならこのままでも問題ありませんが、Rails アプリケーションが初期化されていないので、モデルを検索したり、作成したり、削除したりすることはできません。

Rails アプリケーションの初期化方法と合わせて、モデルを作成する独自コマンドを sample_app のサブコマンドとして追加する方法をみていきます。

まず、main_command.rb を修正してサブコマンドを追加します。

lib/commands/main/main_command.rb
require "rails/command"
require_relative '../user/user_command'
require_relative '../micropost/micropost_command'

module SampleApp
  module Command
    class MainCommand < Rails::Command::Base
      namespace "sample_app"
      @command_name = "sample_app"

      subcommand "user", SampleApp::Command::UserCommand
      subcommand "micropost", SampleApp::Command::MicropostCommand
    end
  end
end

Rails アプリケーションが初期化されていないので、クラスのオートロードは効きません。require_relative を用いて明示的に user_command.rbmicropost_command.rb を読み込む必要があります。クラスを読み込んだ後、Thor の subcommand 命令で usermicropost という2つのサブコマンドを追加しています。

user サブコマンドの実体 user_command.rb は次のように実装します。

lib/commands/user/user_command.rb
module SampleApp
  module Command
    class UserCommand < Rails::Command::Base
      desc "list", "list users."
      def list
        require_application_and_environment!

        say
        say "#{'Name'.ljust(14)}  #{'Email'.ljust(32)}  Updated At"
        say "-" * 80

        User.all.each do |user|
          say "#{user.name.ljust(14)}  #{user.email.ljust(32)}  #{user.updated_at.iso8601}"
        end
      end
    end
  end
end

user_command.rb では、ユーザー一覧を表示する list というコマンドを定義しています。このコマンドは bin/rails sample_app:user list と実行することを意図しています。

list の先頭で require_application_and_environment! を呼び出し Rails アプリケーションを初期化し、続いて User をデータベースから読み込み、コンソールに出力しています。
なお、Rails アプリケーションの初期化後は、オートロードが効くようになるので、User モデルを明示的に読み込む必要はありません。

micropost サブコマンドの実体 micropost_command.rb を次のように実装します。

lib/commands/micropost/micropost_command.rb
module SampleApp
  module Command
    class MicropostCommand < Rails::Command::Base
      desc "list", "list microposts."
      def list
        require_application_and_environment!

        say
        say "#{'Name'.ljust(14)}  #{'Content'.ljust(14)}  Created At"
        say "-" * 80

        Micropost.all.each do |post|
          say "#{post.user.name.ljust(14)}  #{post.content.ljust(14)}  #{post.created_at.iso8601}"
        end
      end
    end
  end
end

ほぼ user_command.rb と同じで、こちらの方は Micropost モデルの一覧をコンソールに出力しています。

サブコマンドを追加できたら試しに実行してみます。

$ bin/rails sample_app:user list

Name            Email                             Updated At
--------------------------------------------------------------------------------
sample          sample@example.jp                 2020-12-05T05:33:11Z

Rails チュートリアルを少し進め、ユーザーを登録したら、上のように出力されます。

rails コマンドのその他の実行方法

単に rails と実行した場合も bundle exec rails などと実行した場合も bin/rails ファイルが実行されますので、bin/rails sample_app:user list に代えて bundle exec rails sample_app:user list と実行することもできます。

要検討・改善点など

  • Rails の作法にならって Rails::Command::Base を継承した ApplicationCommand というクラスを作成し、ApplicationCommand クラスを継承するようにした方が良いのかも?
  • help コマンドがなからず追加されるが、help コマンドを実行するとエラーになる。例 bin/rails sample_app:helpbin/rails sample_app:user help など。
    • 理由は標準の help コマンドが Rails エンジンしかサポートしてない。Rails アプリケーションは全く考慮されていない。
    • 改善方法としては ApplicationCommand クラスで help コマンドを独自実装するのが良いのかなと考えています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Ruby] 自身を実行している処理系の種類を判定する

Ruby という言語には複数の実装があるが、それらをスクリプト上からどのようにして programmatically に見分ければよいだろうか。

Object クラスに定義されている RUBY_ENGINE という定数がこの用途に使える。

参考: Object::RUBY_ENGINE

上記ページの例から引用する:

$ ruby-1.9.1 -ve 'p RUBY_ENGINE'
ruby 1.9.1p0 (2009-03-04 revision 22762) [x86_64-linux]
"ruby"
$ jruby -ve 'p RUBY_ENGINE'
jruby 1.2.0 (ruby 1.8.6 patchlevel 287) (2009-03-16 rev 9419) [i386-java]
"jruby"

それぞれの処理系がどのような値を返すかだが、stack overflow に良い質問と回答があった。

What values for RUBY_ENGINE correspond to which Ruby implementations? より引用:

RUBY_ENGINE Implementation
<undefined> MRI < 1.9
'ruby' MRI >= 1.9 or REE
'jruby' JRuby
'macruby' MacRuby
'rbx' Rubinius
'maglev' MagLev
'ironruby' IronRuby
'cardinal' Cardinal

なお、この質問・回答は 2014年になされたものであり、値は変わっている可能性がある。MRI (aka CRuby) については執筆時現在 (2020/12/8) も 'ruby' が返ってくることを確認済み。

この表にない主要な処理系として、mruby'mruby' を返す。

mruby 該当部分のソース より引用:

/*
 * Ruby engine.
 */
#define MRUBY_RUBY_ENGINE  "mruby"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【個人開発】SOFT SKILLSで紹介されているタスク管理法に特化したWebアプリを作ってみた

Rails1ヶ月チャレンジ 1つ目:PomoTask (タスク管理ツール)

※Railsの勉強として、1ヶ月に1個アプリを作っています

作ったWebアプリのリンク:https://pomo-task.herokuapp.com/

はじめに

SOFT SKILLSで紹介されていた時間管理法をご存知ですか?

このタスク管理法はポモドーロテクニック、カンバンなどを融合していて、自宅での作業が効率的に行えるようになっています。

とても良い方法ですが、不満な点が1つだけありました。それがツールの使いやすさです。SOFT SKILLSではKanban Flowが紹介されていましたが、このサービスでは個人の時間管理に特化しているわけではないので、少し不満が出てきます。

  • 集団での利用を想定して作られているため、個人では使いづらい
  • ポモドーロタイマーが使いづらい
  • タスクの締め切りを把握しにくい
  • 全体のデザインは硬め

この点を解決すべく他のサービスを探していましたが、自分の好みに合うものがなかなか見つかりませんでした。なので自分で作ることに決めました。

また、前からRailsを扱えるようになりたいと思っていたということもあります。毎月新しいプロジェクトを作ると良いと聞いたので、その1つ目です。RubyもRailsも始めて1ヶ月なので至らない点が多いと思いますが、どうかご覧いただければ幸いです。

作ったもの

先日、PomoTaskというWebアプリをリリースしました。

https://pomo-task.herokuapp.com/

localhost_3000_.png

推しポイント

  • ポモドーロタイマーの使い心地
    • START, SKIP, STOPのみの簡単な操作性
    • 何回目かが分かる
    • 音とブラウザ通知で開始、終了1分前、終了をお知らせ
  • 目標
    • その日、週のポモドーロ数が分かる
    • 年、四半期、月、週の目標を確認できる
    • 週目標は常に見られる
  • 締め切りの見やすさ
  • 曜日ルーチン
    • 毎週行うタスクは自動で追加できる
  • 集中力を高めるための仕掛け
    • ポモドーロタイマーの色が変わる
    • 集中力を高めるためのコツをヘルプに掲載
  • 色、視認性
    • タスクの背景色

おわりに

最低限使えるレベルのものが作れたような気がします。とりあえず今後は、新しいWebアプリを作ったり、PomoTaskに機能を追加しようと考えています。

付けるべき機能や改善した方がいい点があれば、コメントしていただけると嬉しいです。

参考資料

  • SOFT SKILLS ソフトウェア開発者の人生マニュアル - ジョン・ソンメズ
  • 自分を操る超集中力 - メンタリストDaiGo
  • どんな仕事も「25分+5分」で結果が出る ポモドーロ・テクニック入門 - フランチェスコ・シリロ (勉強に使った参考書:現場で使える Ruby on Rails 5速習実践ガイド)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker ruby(2.6.5)・mysql(5.6.47)・rails(6.0.0)の開発環境構築

概要

dockerを開発環境に使ってアプリを作成しました。初めてdockerを使ってなかなかうまく動かなかったので動いたものをアウトプットを含めて共有します。また、deviseの導入も行って開発も少々行っていきたいと思います。

dockerの導入

ベースはdockerの公式サイトにRailsとPostgresSQLのdocker-composeの使い方が乗っていたのでそちらを参考にします。

dockerの公式サイト
https://docs.docker.com/compose/rails/

ただ、rails6.0.0からはwebpackerが標準になったことにより6.0.0を使う際には修正が必要です。また、今回はmysqlを使うのでそちらも変更していきます。

アプリのディレクトリ作成

ターミナル.
mkdir sampleapp

アプリのディレクトリに移動

ターミナル.
cd sampleapp 

dockerfileの生成

ターミナル.
touch dockerfile

dockerfileの編集

作成したdockerfileを以下のように編集します。

dockerfile.
FROM ruby:2.6.5

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list

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

RUN mkdir /myapp

WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
COPY . /myapp

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]

公式の以下の点を修正しました。

1.rubyのバージョンを変更。
2.webpackerが標準になったことにより、必要になったyarnのインストールを行う。

FROM ruby:2.6.5

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list

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

entrypoint.shの生成

 touch entrypoint.sh

entrypoint.shの編集

作成したentrypoint.shを編集します。こちらはdockerの公式サイトの通りになります。

entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

docker-compose.ymlの生成

touch docker-compose.yml

docker-compose.ymlの編集

生成したファイルを以下のように編集します。

docker-compose.yml
version: "3"
services:
  db:
    image: mysql:5.6.47
    environment:
        MYSQL_ROOT_PASSWORD: password
        MYSQL_DATABASE: root
    ports:
        - "3306:3306"
    volumes:
        - ./db/mysql/volumes:/var/lib/mysql
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    stdin_open: true
    tty: true
    volumes:
      - .:/myapp
      - gem_data:/usr/local/bundle
    ports:
      - "3000:3000"
    depends_on:
      - db
volumes:
  mysql_data:
  gem_data:

公式のリファレンスではpostgresqlを使っているためmysqlにしています。

services:
db:
image: mysql:5.6.47
environment:
MYSQL_USER: root
MYSQL_ROOT_PASSWORD: password
ports:
- "3306:3306"
volumes:
- ./db/mysql/volumes:/var/lib/mysql

アプリ製作中にbinding.pryを使えるように以下を追加しています。

stdin_open: true
tty: true

データとgemを永続化するために以下の記述を追加しています。

volumes:
mysql_data:
gem_data:

Gemfileの生成

ターミナル.
touch Gemfile

生成したファイルを以下のように編集します。

Gemfile.
source 'https://rubygems.org'
gem 'rails', '~>6'

空のGemfile.lockを生成

ターミナル.
touch Gemfile.lock

railsのプロジェクトを作成

ターミナル.
docker-compose run web rails new . --force -d mysql

docker-composeをbuildする

ターミナル.
docker-compose build

database.ymlの修正

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

docker-composeの再起動

docker-composeを一旦downさせてバックグラウンドで起動するようにコマンドを入力

ターミナル.
docker-compose down
docker-compose up -d

データベースの作成

ターミナル.
docker-compose run web bundle exec rails db:create

ここまできたらlocalhost:3000/にアクセスしてみましょう。以下の画面が表示されるはずです。

スクリーンショット 2020-12-08 16.14.11.png

deviseを用いた簡単なアプリの開発

ここからはアプリ開発の入り口をやっていきます。

コントローラーとビューを生成

トップページを表示するためにposts(投稿)コントローラーとindexのビューを生成します

ターミナル.
docker-compose run web rails g controller posts index

生成したらルーティングを設定します。routes.rbを以下のように編集しましょう。

config/routes.rb
Rails.application.routes.draw do
  root 'posts#index'
  get 'posts/index'
end

この時点でlocalhost:3000/にアクセスすると以下のような画面になっています。
先ほど作成したpostsのindex(view)が表示されていることがわかります。

スクリーンショット 2020-12-08 16.19.20.png

deviseの導入

gemファイルの一番したにdeviseを記述します。

Gemfile.
gem 'devise'

bundle installを行う

dockerを利用している場合には通常行っているコマンドに "docker-compose run web"をつける必要があります。

ターミナル.
docker-compose run web bundle install

dockerの再起動

gemを新たに追加したためdocker-composeの再起動を行います。

ターミナル.
docker-compose down
docker-compose up -d

あとは通常のdeviseと同じ作業を行っていきます。
それぞれのコマンドを入力していきます。

ターミナル.
docker-compose run web rails g devise:install
docker-compose run web rails g devise user
docker-compose run web rails db:migrate
docker-compose run web rails g devise:views

ここまできたら再度dockerを再起動させます

ターミナル.
docker-compose down
docker-compose up -d

ここまででdeviseの導入全てが終わったためあとはposts/index.html.erbに以下を記述してみましょう。

posts/index.html.erb
<%=link_to "ログイン", new_user_session_path %>

ここまででlocalhost:3000/にアクセスするとログインボタンが表示され、押すとログイン画面に遷移します。
これでdockerを使ってdeviseを動かすところまで行えました。
ここからはそれぞれのオリジナルアプリの仕様に従って開発ができると思います。
トップページ
スクリーンショット 2020-12-08 16.30.45.png

ログインページ
スクリーンショット 2020-12-08 16.33.27.png

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

[Rails]複数モデルの検索機能

はじめに

検索機能をつけていきたいと思います。
簡易的なものですがポートフォリオ作成等の参考にしてください。

今回は検索対象をユーザーか投稿かを選べるようにする且、検索方法を完全一致・部分一致から選択できるようにしたいと思います。(前方一致・後方一致もアレンジで付け加えられるように少しだけ説明します。)
また検索結果を一覧にして新しいページに表示させます。

尚、gem ransackは使いません。

こんな人に向けて

1.検索機能の大枠を実装したい人。
2.gem ransackを使いたくない人。
3.実装方法のイメージが湧かない初学者。

1.実装をはじめる前に

まずどのように検索していくかの手順を先に説明していきます。

①検索フォームで入力内容を受け取り、コントローラに送る
②コントローラとモデルがデータベースからデータを受け取る。
③ビューに表示させる。

簡単にまとめると上記のような流れです。

またこれから実装するにあたって
if-else文
whereメソッド
の2点は基本であり重要でもあります。
分からない人は参考になるサイトがネットにたくさんあるので、そちらを見ながら進めてください。

2.検索機能の実装

2-1.コントローラーの作成

それでは実装していきたいと思います。
まずコントローラーを作成します。

コントローラ名とアクション名は分かりやすければ何でもOKです。(searchやfinder等)
今回はFindersコントローラにfinderアクションをつくります。

ターミナル
rails g controller Finders finder

これでコントローラ・アクション・ビューが自動作成されました。

2-2.ルートの設定

検索ボタンが押された際にどこのコントローラの何のアクションにリクエストが飛ぶのか設定します。
尚、finderアクションを使っているためresourcesは使用できません。

config/routes.rb
get 'finder' => "finders#finder"

ターミナルでrails routesと入力してみましょう。
finder GET /finder(.:format) finders#finderと出てくるはずです。
これでfinder_pathでリクエストされた際に、findersコントローラのfinder
アクションに飛ぶようになりました。

2-3.検索フォームの作成

アクションの中身を書く前に検索フォームから作成します。
form_withを使って入力された内容を受け取っていきます。
検索フォームを設置するページは人によって異なるので、ひとまずはapplication.html.erbに書きます。
別のページに設置したとしても後の記述は変わりません。

app/views/layouts/application.html.erb
<%= form_with url: finder_path, method: :get, local: :true do |f| %>
      <%= f.select :range, options_for_select([['User'], ['Post']]) %>
      <%= f.select :search, options_for_select([["完全一致","perfect_match"], ["部分一致","partial_match"]]) %>
      <%= f.text_field :word %>
      <%= f.submit "検索" %>
<% end %>

分解して説明します。

<%= form_with url: finder_path, method: :get, local: :true do |f| %>
    //findsコントローラのfindアクションに送る情報
<% end %>

form_with ~ do |f|は入力内容を受け取る定型文です。
最後のfの部分はformでもaでも大丈夫ですが、多くの人はfを使っています。

url: finder_pathで受け取る情報をどこに送るかを指定しています。

method: :getはgetメソッドを使うことを宣言しています。
rails routesfinder GET /finder(.:format) finders#finderとあるようにGETメソッドが指定されています。

local: :true同期通信なのでこのように書きます。

<%= f.select :range, options_for_select([['User'], ['Post']]) %>
<%= f.select :search, options_for_select([["完全一致","perfect_match"], ["部分一致","partial_match"]]) %>

f.select :range:選択されたものをrangeとしてアクションに送るようにしています。
f.selectdo |f|の記述によって変わり、do |form|とするならばform.selectとする必要があります。

options_for_select( [ ['要素1'],['要素2'] ] ):これでタブが作成されます。
要素1・要素2は文字列として扱うのでシングル(ダブル)クォーテーションで囲んでください。
options_for_select( [ ['要素1','A'], ['要素2','B'] ] )とすることも可能で、要素1とAは同じものとして扱われます。タブには最初の要素1・要素2が表示されます。
また2つの要素だけでなくもっと増やしたい場合は
( [ ['要素1'],['要素2'],['要素3'],['要素4']・・・ ] )としてください。

<%= f.text_field :word %>

入力されたものをwordとしてアクションに送るようにしています。

<%= f.submit "検索" %>

入力結果を送信します。"検索"でボタン内の文字を変えています。

検索フォームは完成です。

2-4.モデルの追記

finderアクションで使用する、searchesとwordsの引数を受け取るlooksメソッドをモデルに作成します。

app/models/user.rb
def self.looks(searches, words)
    if searches == "perfect_match"
      @user = User.where("name LIKE ?", "#{words}")
    else
      @user = User.where("name LIKE ?", "%#{words}%")
    end
end

まず条件分岐させ検索方法を変えます。
またwhereメソッドを使いデータベースから該当するのものを全て受け取り@userに保管します。

2-3で説明したように要素を増やし、条件分岐を加え、"#{words}"の部分を書き換えることで前方一致や後方一致の検索もできます。

2-5.アクションの記述

app/controllers/finders_controller.rb
  def finder
    @range = params[:range]
    search = params[:search]
    word = params[:word]
  @users = User.looks(search, word)
  end

先ほどのフォームで入力された情報をここで受け取ります。
@range = params[:range]search = params[:search]word = params[:word]:それぞれ検索フォームで選択・入力された情報を変数に代入しています。後にビューでも使うので@range@usersはインスタンス変数にしています。

@users = User.looks(search, word)

2-4で作ったlooksメソッドを使い、検索結果を@usersに代入しています。

フォームで選択された検索方法は
f.select :searchparams[:search]searchUser.looks(search, word)def self.looks(searches, words)if searches == "perfect_match"
と送られていることになります。

params[:search]searchUser.looks(search, word)の部分をまとめて

app/controllers/finders_controller.rb
def finder
    @range = params[:range]
    @users = User.looks(params[:search], params[:word])
end

とすることもできます。上記の書き方で進めていきます。

2-6.アクション内での条件分岐

2-5のままではユーザーの検索しかできません。
なのでif文を使い、ユーザーか投稿かを切り替えられるようにします。

app/controllers/finders_controller.rb
  def finder
    @range = params[:range]

    if @range == "User"
      @users = User.looks(params[:search], params[:word])
    else
      @posts = Post.looks(params[:search], params[:word])
    end
  end

分解して説明します。

if @range == "User"
  //ユーザーから探す処理(user.rbのlooksメソッドを使用)
else
  //投稿から探す処理(post.rbのlooksメソッドを使用)
end

@rangeにはUserかPostが入っています。
それをif-else文で分けて各モデルのlooksメソッドを使います。

2-5でuser.rbに作成したlooksメソッドを一部書き換えてapp/models/post.rbにも記述します。

app/models/post.rb
def self.looks(searches, words)
    if searches == "perfect_match"
      @post = Post.where("name LIKE ?", "#{words}")
    else
      @post = Post.where("name LIKE ?", "%#{words}%")
    end
end

これで検索対象も切り替えられるようになりました。

最後にビューを作成して完成です。

2-7.ビューの作成

検索結果一覧を表示するページをつくります。
既存のページに表示することもできますが、今回は新しくページを作成します。

app/views/finders/finder.html.erb
<% if @range == "User" %>
    <% @users.each do |user| %>
        <%= user.name %>    //例(ユーザーの名前を表示)
    <% end %>
<% else %>
    <% @posts.each do |post| %>
        <%= post.title %>   //例(投稿のタイトルを表示)
        <%= post.text %>    //例(投稿の本文を表示)
    <% end %>
<% end %>

分解して説明します。

<% if @range == "User" %>
    //検索対象がUserのとき、ユーザーを一覧表示
<% else %>
    //検索対象がPostのとき、投稿を一覧表示
<% end %>

アクションと同じようにif-else文で条件分岐させています。

<% @users.each do |user| %>
    <%= user.name %>    //例(ユーザーの名前を表示)
<% end %>
--------------------------------------------------
<% @posts.each do |post| %>
    <%= post.title %>   //例(投稿のタイトルを表示)
    <%= post.text %>    //例(投稿の本文を表示)
<% end %>

finderアクションでそれぞれ変数定義しましたが、@users @postsには検索に該当するデータが全て含まれています。
それをeach文で繰り返し表示させるよう指示しています。

これで完成です。

3.最後に

今回は検索機能の大枠をつくりました。これを活かして細かい部分はアレンジができます。
ぜひ試してください。

また実装方法はいろいろありますので、当記事だけでなく他の記事も参考にしてみてください。

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

検索方法・検索対象を選択できる検索機能を実装する方法(ransack未使用)

はじめに

検索機能をつけていきたいと思います。
簡易的なものですがポートフォリオ作成等の参考にしてください。

今回は検索対象をユーザーか投稿かを選べるようにする且、検索方法を完全一致・部分一致から選択できるようにしたいと思います。(前方一致・後方一致もアレンジで付け加えられるように少しだけ説明します。)
また検索結果を一覧にして新しいページに表示させます。

尚、gem ransackは使いません。

こんな人に向けて

1.検索機能の大枠を実装したい人。
2.gem ransackを使いたくない人。
3.実装方法のイメージが湧かない初学者。

1.実装をはじめる前に

まずどのように検索していくかの手順を先に説明していきます。

①検索フォームで入力内容を受け取り、コントローラに送る
②コントローラとモデルがデータベースからデータを受け取る。
③ビューに表示させる。

簡単にまとめると上記のような流れです。

またこれから実装するにあたって
if-else文
whereメソッド
の2点は基本であり重要でもあります。
分からない人は参考になるサイトがネットにたくさんあるので、そちらを見ながら進めてください。

2.検索機能の実装

2-1.コントローラーの作成

それでは実装していきたいと思います。
まずコントローラーを作成します。

コントローラ名とアクション名は分かりやすければ何でもOKです。(searchやfinder等)
今回はFindersコントローラにfinderアクションをつくります。

ターミナル
rails g controller Finders finder

これでコントローラ・アクション・ビューが自動作成されました。

2-2.ルートの設定

検索ボタンが押された際にどこのコントローラの何のアクションにリクエストが飛ぶのか設定します。
尚、finderアクションを使っているためresourcesは使用できません。

config/routes.rb
get 'finder' => "finders#finder"

ターミナルでrails routesと入力してみましょう。
finder GET /finder(.:format) finders#finderと出てくるはずです。
これでfinder_pathでリクエストされた際に、findersコントローラのfinder
アクションに飛ぶようになりました。

2-3.検索フォームの作成

アクションの中身を書く前に検索フォームから作成します。
form_withを使って入力された内容を受け取っていきます。
検索フォームを設置するページは人によって異なるので、ひとまずはapplication.html.erbに書きます。
別のページに設置したとしても後の記述は変わりません。

app/views/layouts/application.html.erb
<%= form_with url: finder_path, method: :get, local: :true do |f| %>
      <%= f.select :range, options_for_select([['User'], ['Post']]) %>
      <%= f.select :search, options_for_select([["完全一致","perfect_match"], ["部分一致","partial_match"]]) %>
      <%= f.text_field :word %>
      <%= f.submit "検索" %>
<% end %>

分解して説明します。

<%= form_with url: finder_path, method: :get, local: :true do |f| %>
    //findsコントローラのfindアクションに送る情報
<% end %>

form_with ~ do |f|は入力内容を受け取る定型文です。
最後のfの部分はformでもaでも大丈夫ですが、多くの人はfを使っています。

url: finder_pathで受け取る情報をどこに送るかを指定しています。

method: :getはgetメソッドを使うことを宣言しています。
rails routesfinder GET /finder(.:format) finders#finderとあるようにGETメソッドが指定されています。

local: :true同期通信なのでこのように書きます。

<%= f.select :range, options_for_select([['User'], ['Post']]) %>
<%= f.select :search, options_for_select([["完全一致","perfect_match"], ["部分一致","partial_match"]]) %>

f.select :range:選択されたものをrangeとしてアクションに送るようにしています。
f.selectdo |f|の記述によって変わり、do |form|とするならばform.selectとする必要があります。

options_for_select( [ ['要素1'],['要素2'] ] ):これでタブが作成されます。
要素1・要素2は文字列として扱うのでシングル(ダブル)クォーテーションで囲んでください。
options_for_select( [ ['要素1','A'], ['要素2','B'] ] )とすることも可能で、要素1とAは同じものとして扱われます。タブには最初の要素1・要素2が表示されます。
また2つの要素だけでなくもっと増やしたい場合は
( [ ['要素1'],['要素2'],['要素3'],['要素4']・・・ ] )としてください。

<%= f.text_field :word %>

入力されたものをwordとしてアクションに送るようにしています。

<%= f.submit "検索" %>

入力結果を送信します。"検索"でボタン内の文字を変えています。

検索フォームは完成です。

2-4.モデルの追記

finderアクションで使用する、searchesとwordsの引数を受け取るlooksメソッドをモデルに作成します。

app/models/user.rb
def self.looks(searches, words)
    if searches == "perfect_match"
      @user = User.where("name LIKE ?", "#{words}")
    else
      @user = User.where("name LIKE ?", "%#{words}%")
    end
end

まず条件分岐させ検索方法を変えます。
またwhereメソッドを使いデータベースから該当するのものを全て受け取り@userに保管します。

2-3で説明したように要素を増やし、条件分岐を加え、"#{words}"の部分を書き換えることで前方一致や後方一致の検索もできます。

2-5.アクションの記述

app/controllers/finders_controller.rb
  def finder
    @range = params[:range]
    search = params[:search]
    word = params[:word]
  @users = User.looks(search, word)
  end

先ほどのフォームで入力された情報をここで受け取ります。
@range = params[:range]search = params[:search]word = params[:word]:それぞれ検索フォームで選択・入力された情報を変数に代入しています。後にビューでも使うので@range@usersはインスタンス変数にしています。

@users = User.looks(search, word)

2-4で作ったlooksメソッドを使い、検索結果を@usersに代入しています。

フォームで選択された検索方法は
f.select :searchparams[:search]searchUser.looks(search, word)def self.looks(searches, words)if searches == "perfect_match"
と送られていることになります。

params[:search]searchUser.looks(search, word)の部分をまとめて

app/controllers/finders_controller.rb
def finder
    @range = params[:range]
    @users = User.looks(params[:search], params[:word])
end

とすることもできます。上記の書き方で進めていきます。

2-6.アクション内での条件分岐

2-5のままではユーザーの検索しかできません。
なのでif文を使い、ユーザーか投稿かを切り替えられるようにします。

app/controllers/finders_controller.rb
  def finder
    @range = params[:range]

    if @range == "User"
      @users = User.looks(params[:search], params[:word])
    else
      @posts = Post.looks(params[:search], params[:word])
    end
  end

分解して説明します。

if @range == "User"
  //ユーザーから探す処理(user.rbのlooksメソッドを使用)
else
  //投稿から探す処理(post.rbのlooksメソッドを使用)
end

@rangeにはUserかPostが入っています。
それをif-else文で分けて各モデルのlooksメソッドを使います。

2-5でuser.rbに作成したlooksメソッドを一部書き換えてapp/models/post.rbにも記述します。

app/models/post.rb
def self.looks(searches, words)
    if searches == "perfect_match"
      @post = Post.where("name LIKE ?", "#{words}")
    else
      @post = Post.where("name LIKE ?", "%#{words}%")
    end
end

これで検索対象も切り替えられるようになりました。

最後にビューを作成して完成です。

2-7.ビューの作成

検索結果一覧を表示するページをつくります。
既存のページに表示することもできますが、今回は新しくページを作成します。

app/views/finders/finder.html.erb
<% if @range == "User" %>
    <% @users.each do |user| %>
        <%= user.name %>    //例(ユーザーの名前を表示)
    <% end %>
<% else %>
    <% @posts.each do |post| %>
        <%= post.title %>   //例(投稿のタイトルを表示)
        <%= post.text %>    //例(投稿の本文を表示)
    <% end %>
<% end %>

分解して説明します。

<% if @range == "User" %>
    //検索対象がUserのとき、ユーザーを一覧表示
<% else %>
    //検索対象がPostのとき、投稿を一覧表示
<% end %>

アクションと同じようにif-else文で条件分岐させています。

<% @users.each do |user| %>
    <%= user.name %>    //例(ユーザーの名前を表示)
<% end %>
--------------------------------------------------
<% @posts.each do |post| %>
    <%= post.title %>   //例(投稿のタイトルを表示)
    <%= post.text %>    //例(投稿の本文を表示)
<% end %>

finderアクションでそれぞれ変数定義しましたが、@users @postsには検索に該当するデータが全て含まれています。
それをeach文で繰り返し表示させるよう指示しています。

これで完成です。

3.最後に

今回は検索機能の大枠をつくりました。これを活かして細かい部分はアレンジができます。
ぜひ試してください。

また実装方法はいろいろありますので、当記事だけでなく他の記事も参考にしてみてください。

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

Gem 'Gimei'

Gimeiとは

日本人の名前やフリガナ、住所などを自動生成してくれるGem。
有名なGemでFakerがあるが、Fakerでは対応できないフリガナを使うことができる。
Gimei

使い方

開発環境とテスト環境で利用するのでgroup :development, :test doの内部でgemを指定、Gemfileを編集したらアプリケーションのディレクトリでbundle installを実行。

Gemfile
group :development, :test do
  # 省略
  gem 'rspec-rails'
  gem 'factory_bot_rails'
  gem 'faker'
  gem 'gimei'
end

コンソールで実行するとこんな感じで名前が生成される。

console
[1] pry(main)> japanese_user = Gimei.name
# 省略
[2] pry(main)> japanese_user.last.kanji
=> "島村"
[3] pry(main)> japanese_user.last.katakana
=> "シマムラ"

FactoryBotと組み合わせて架空のユーザーを生成する。
インスタンスを生成せずにGimei.name.first.kanjiなどを入れると、名前とフリガナが一致しなくなる。

factories/users.rb
FactoryBot.define do
  factory :user do
    # インスタンスを生成
    japanese_user = Gimei.name

    first_name { japanese_user.first.kanji }
    first_name_kana { japanese_user.first.katakana }
    last_name { japanese_user.last.kanji }
    last_name_kana { japanese_user.last.katakana }
  end
end
console
[1] pry(main)> FactoryBot.create(:user)
# 以下実行結果が表示される。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】表の合計値算出方法(aggregate関数)

はじめに

Railsで表の合計値を算出するにあたって、aggregate関数なるものが便利でスマートだったので、記事にしてみました。

開発環境

IDE:Cloud9
Ruby:2.6.3
Rails:5.2.4

実例をみてみる

やりたいこと

このような表を作成し、下に合計値を表示したい。
_2020-11-15_17.21.26.png

ER図

_2020-12-08_14.20.07.png

aggregate関数を使わない場合

controllers/carts.rb
def new
  @carts = Cart.where(user_id: current_user.id)
end

例えば、たんぱく質(protain)の合計値を表示したい場合

views/carts/new.html.rb
<% sum = 0 %>
<% @carts.each do |cart| %>
<% sum += cart.food.protain %>
<% end %>
<%= sum %>

これだと、1つの項目(上記の場合たんぱく質)を表示するのに、5行も必要となってしまい、見ため的にあまりスマートとはいえない。

views/carts/new.html.rb
<td>合計</td>
<td>
    <% sum = 0 %>
    <% @carts.each do |cart| %>
    <% sum += cart.food.calorie %>
    <% end %>
    <%= sum %>
</td>
<td>
    <% sum = 0 %>
    <% @carts.each do |cart| %>
    <% sum += cart.food.protain %>
    <% end %>
    <%= sum %>
</td>
<td>
  <% sum = 0 %>
    <% @carts.each do |cart| %>
    <% sum += cart.food.fat %>
    <% end %>
    <%= sum %>
</td>
<td>
  <% sum = 0 %>
    <% @carts.each do |cart| %>
    <% sum += cart.food.carbon %>
    <% end %>
    <%= sum %>
</td>

表全体を表示すると、割とfat感がある。
そこでaggregate関数を用いて、もっとスマートに記述する。

aggregate関数を使う方法

コントローラはさっきと一緒。

controllers/carts.rb
def new
  @carts = Cart.where(user_id: current_user.id)
end

カートモデルに以下を記述する。

model/cart.rb
def self.aggregate(column)
  self.all.map { |cart| cart.food[column] }.sum
end
views/carts/new.html.rb
<td>合計</td>
<td>
  <%= @carts.aggregate(:calorie) %>
</td>
<td>
  <%= @carts.aggregate(:protain) %>
</td>
<td>
  <%= @carts.aggregate(:fat) %>
</td>
<td>
  <%= @carts.aggregate(:carbon) %>
</td>

めっちゃすまーと。

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

RSpec 独自バリデーションのテストで少しハマった話

はじめに

RSpecのテストを記述中sizeメソッドとcountメソッドの違いを知らず少しハマったのでメモして行きます。

Rails 6.0.3.4
ruby 2.6.3p62
RSpec 3.10

テスト内容

親モデルのuserは子モデルhabitを複数登録できる 1対多の関係。
そこでuserhabitモデルを6つまでしか登録できないという独自のバリデーションを加えてあります。

habit.rb

class Habit < ApplicationRecord
belongs_to :user

validates :task, presence: true, length: { maximum: 12 }, uniqueness: { scope: :user }
validates :frequency, presence: true
validate :user_habits_size_validate

HABIT_MAX = 6
def user_habits_size_validate
    if self.user && self.user.habits.size >=  HABIT_MAX
        errors.add(:task,"は#{HABIT_MAX}つまでしか登録出来ません")
    end
end
end

このバリデーションが正常に機能してるかのテストを書こう思い、このように書いてみました。
(FactroyBotでuserhabitは作成済み)

habit_spec.rb

it 'userはhabitモデルを6つ以上持てない' do
    habits = create_list(:habit, 6, user: user)
    expect(build(:habit, task: "筋トレ", user: user).save).to be_falsey
end

create_listhabitを6つ作成し、7つ目ではfalseが返る。
これで実行してみると

1) Habit バリデーション userはhabitモデルを6つ以上持てない
     Failure/Error: expect(build(:habit, task: "筋トレ", user: user).save).to be_falsey

       expected: falsey value
            got: true

trueが返ってきた

原因はhabit.rbsizeメソッドにありました。

self.user.habits.size >= HABIT_MAX

このsizeメソッドはキャッシュを参照している。

つまり self.user.habits.size >= HABIT_MAXこの式は常にfalseになるのでいくらhabitを生成してもバリデーションはかからなかった。

なので,countメソッドを使いました。 countは常にSQLを発行して確認する。

self.user.habits.count >= HABIT_MAX

これでテストを走らせると

.....
Finished in 0.40233 seconds (files took 1.41 seconds to load)
5 examples, 0 failures

無事通りました。
ちなみにこうしてもテストは通りました。↓
self.user.habits.reload.size >= HABIT_MAX

.....
Finished in 0.41723 seconds (files took 1
.43 seconds to load)
5 examples, 0 failures

最後に

書いていて気づいたんですが、そもそもテスト通すためにhabit.rbの方をいじるのは違くないか?と思い、また試行錯誤中です。

まだまだ勉強中ですので間違いなどありましたらご指摘いただけると幸いですm(__)m

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

【初心者】Rubyでbinding.pryを使う方法【絶対使うべき】

概要

勉強会で、Rubyで「binding.pry」を使えるようにする方法を学びましたので、まとめました。

設定方法

①Gemfileを作成

ターミナル
 touch Gemfile

Railsではなく、rubyのフォルダにGemfileを作るという考え方がなかったので、驚きました。
Github:https://github.com/pry/pry

②Gemfileを記述

githubを参考に、コピペします

Gemfile
gem 'pry', '~> 0.13.1'

③インストール

ターミナル
bundle install

④設定

main.rb
#pryを読み込みたいファイルに記述する
require'pry'

⑤使い方

main.rb
#止めたいところに記述
binding.pry

上記を入力してある状態でコードを実行すると
「binding.pryを記述したところ」でとまるので、以下のようなことを試してください。

  • 変数に何が格納されているか?
  • 期待している値は入っているか?
  • binding.pryの部分で止まるのか?そうでない部分でエラーが出るか?

以上になります。

まとめ

binding.pryを使えるようになって、putsやpに出力させる必要がなくなり、開発効率が上がりました。
難しそうと思って使っていなかった過去の自分を叱りたいです。
使ったことがない方は使ってみてください。

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

sliceメソッドを使ったAPI問題

本日はこちらの問題を解いていきます。

 
問題.
任意の文字列の最後の2文字を3回繰り返し
出力するメソッドを作りましょう。

出力例:
extra_end('Hello') → 'lololo'
extra_end('ab') → 'ababab'
extra_end('Hi') → 'HiHiHi'

ヒント
sliceメソッドを用いることで、配列や文字列から指定した要素を取り出すことができます。

# 配列を作成します
array = [0,1,2,3,4,5,6]

# 配列から引数で指定した要素をsliceします
ele1 = array.slice(1)
puts ele1
#=> 1

# 配列番号1から4つ分の要素をsliceします
ele2 = array.slice(1,4)
puts ele2
#=> 1 2 3 4

# 配列はもとのままです
puts array 
#=> [0,1,2,3,4,5,6]

模範回答

def extra_end(str)
  char_num = str.length
  right2 = str.slice(char_num - 2, 2)
  puts right2 * 3
end

解説
たとえば、extra_end('Hello')でメソッドを呼び出した場合、
.lengthを使うことによって1から数えることが出来ます。(使わないと0から数えてしまいます。)
char_num = 5となり、right2 = str.slice(3,2)になります。
slice(3,2)は配列番号(インデックス)3つ目から数えて2つ分の要素を切り取ります。
今回の場合、切り取られた結果loが残り、right2 = loとなります。
最終的に、right2 * 3、つまりlololoと出力されます。

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

OpenAPIのYAML分割管理と構成案

はじめに

READYFORのエンジニアリング部に所属している熊谷です。

この記事はREADYFOR Advent Calendar 2020の8日目の記事です。

概要

スキーマー駆動開発でOpenAPI(旧Swagger)を導入し始めたところなのですが、その中で、OpenAPIの運用管理について色々調査・検討していたので、記事として共有させていただきます。

対象読者

以下の方々を対象としています

  • API開発でOpenAPI導入を検討している方。
  • 既にOpenAPIの導入済みの方。

背景 ( 課題感 )

スキーマー駆動開発でOpenAPI(旧Swagger)を採用している企業は多いかと思いますが、OpenAPI導入において最初に感じた課題感として、陥りそうな状況の一つとして、最初に運用方針を決めないまま、多数のメンバーが一つのopenapi.yamlにスキーマー定義を追加・更新していった場合、

  1. ファイルサイズが膨れあがり、
  2. スキーマー定義も一貫性がなくなり、
  3. 運用管理が覚束なくなる、

みたいなケースが想定されるのではと思いました。

そのため、予めそのようなサービス拡大にも耐えられるように、また、マイクロサービスなど複数サービスにも対応できるように、OpenAPIの運用方針・構成を考えみました。(一部、実際に運用開始しています)

OpenAPIの構成

OpenAPI専用のGitリポジトリを作成し、下記のような構成で構築します。
( 記事の最後にサンプルgitのリンクを貼っています )

構成イメージ

全体のディレクトリ構成のイメージです。

openapi.yamlは、直接編集するのではなく、openapi-generatorを使って中間ファイルから生成するようにします。中間ファイルを用いることでYAMLを分割して定義することができるようになります。

Screen Shot 2020-12-06 at 23.02.11.png

ディレクトリ構成

具体的には、下記のようなディレクトリ構成になります。

./
├── README.md
├── openapi
│   ├── {サービス名}
│   │   └── openapi.yaml
│   └── api(ex)
│       └── openapi.yaml
└── src
    ├── components:全体の共通コンポーネント
    └── services:
        └── {サービス名}
            ├── root.yaml:中間ファイル
            ├── paths:各エンドポイントのスキーマー定義
            └── examples:Example用YAML
        ├── api(ex)
            ├── root.yaml:中間ファイル
            ├── paths:各エンドポイントのスキーマー定義
            │   ├── animals
            │   │   ├── cats.yaml
            │   │   └── dogs.yaml
            │   └── fruits
            │       └── apples.yaml
            ├── examples:Example用YAML
            │   ├── animals-cats-example-1.yaml
            │   ├── animals-dogs-example-1.yaml
            │   └── fruits-apples-example-1.yaml
├── scripts:各種生成スクリプト群
    ├── openapi2generator-ruby.sh
    ├── root2openapi.sh
    └── swagger-ui.sh

ディレクトリ概要

各ディレクトリの概要と、そこに配置するYAMLファイル名のフォーマットです。

ディレクトリ名 概要 YAMLファイル名
openapi OpenAPIファイル群
・中間ファイルから自動生成されたファイル群
・直接このファイルは修正することはない
openapi.{サービス名}.yaml
src/services 中間ファイル群
・このファイルを元に./openapi/配下のyamlを生成する。
{サービス名}.yaml
src/services/*/paths スキーマー定義ファイル群
・スキーマー定義が記述されている。
・中間YAMLから参照される。
{タグ名}/{エンドポイント名}.yaml
src/services/*/paths/*/components タグの共通コンポーネント用ファイル群 {コンポーネント名}.yaml
src/services/*/examples Exampleファイル群
(アンダースコアやディレクトリを用いると上手く生成されないためハイフンで繋げる)
{タグ名}-{エンドポイント名}-example-{No}.yaml
src/components 全体の共通コンポーネント用ファイル群
・ページング情報、バリデーション、認証情報など全社的に共通フォーマットと定義した方がいいようなもの。
{コンポーネント名}.yaml
scripts スクリプトファイル群
・openapi.yamlやRubyコード生成スクリプトなど。

各ファイル記述

各YAMLファイルに記述する内容を順に紹介します。

中間ファイル

./src/services/*.yaml

  • このディレクトリには中間ファイルを配置します。
  • 中間ファイルには、サービス概要とエンドポイント一覧のみを記述します。
  • (各エンドポイントのスキーマー定義は記述しません。)
項目 説明
openapi 3.0.0
info openapiの基本情報
servers テストで使用するサーバー情報を記述する
tags 各ドメインの概要を記述する 補足:
タグ名=ドメインとして定義する。
paths 各エンドポイント一覧を記述する フォーマット:
$ref: ./paths/{タグ名}/{エンドポイント名}.yaml
src/services/api.yaml
openapi: 3.0.0
info:
  title: XXXX API
  description: "XXXX Service API"
  version: '1.0'
  contact:
    name: XXXX Service API
    url: 'https://xxx..jp'
    email: xxx@xxxxx.xx
  termsOfService: 'https://xxxx.xx/terms'
servers:
  - url: 'http://localhost:3000'
    description: development
  ...
tags:
  - name: animals
    description: 動物
  - name: fruits
    description: 果物
paths:
  # Animals: 動物
  /animals/cats:
    $ref: ./paths/animals/cats.yaml
  /animals/dogs:
    $ref: ./paths/animals/dogs.yaml
  /animals/dogs:
    $ref: ./paths/animals/rabbits.yaml
  /animals/rabbits:
  ...
  # Fruits: 果物
  /fruits/apples:
    $ref: ./paths/resources/apples.yaml
  /fruits/oranges:
    $ref: ./paths/fruits/oranges.yaml

スキーマー定義ファイル

./src/services/{サービス名}/paths/{タグ名}/*.yaml

  • 各エンドポイントごとにスキーマー定義を記述します。
  • operationIdや各オブジェクト名は、コンフリクトを起こさないように、一貫性を持たせます。
  • examplesを同じファイルに纏めると、見通しが悪くなるため、refs参照を使い、別ファイルに分けて管理します。
項目 説明 フォーマット
operationId エンドポイントのユニークID {タグ名}_{エンドポイント名}
{タグ名}_{エンドポイント名}_{メソッド名}
・CURDなど複数メソッドに対応する場合
summary エンドポイント名のタイトルを記述する -
description エンドポイント名の詳細仕様を記述する。
・なるべく丁寧に詳細に記述する。
-
parameters リクエストのスキーマーを定義する $refs名:
{operationId}_Params"
{operationId}_{オブジェクト名}Params"
responses レスポンスのスキーマーを定義する $refs名:
・第一階層 = {operationId}
・第二階層以下 = {operationId}_{オブジェクト名}
responses
.examples
.example
テストデータのYAMKファイルを指定する $refs:
{タグ名}-{エンドポイント名}-example-{No}.yamll
・ハイフンやディレクトリ構成は不可のためハイフンで繋げる。
properties プロパティ名 ・ローワーキャメルケースで記述する。(TSの都合上)
・user_id → userId
requiered 必須項目 必須
/paths/animals/dogs.yaml
get:
  summary: 犬一覧を取得する
  operationId: Animals_DogsGet
  description: |
    xxxxxxxxxxxxxxxx
  tags:
    - animals
  responses:
    "200":
      content:
        application/json:
          schema:
            $ref: Animals_DogsGet
          examples:
            example_1:
              $ref: '../../examples/animals-dogs-get-example-1.yaml'
            example_2:
              $ref: '../../examples/animals-dogs-get-example-2.yaml'
post:
  summary: 犬一覧を取得する
  operationId: Animals_DogsPost
  description: |
    xxxxxxxxxxxxxxxx
  tags:
    - animals
  responses:
    "200":
      content:
        application/json:
          schema:
            $ref: Animals_DogsGet
          examples:
            example_1:
              $ref: '../../examples/animals-dogs-post-example-1.yaml'
components:
  schemas:
    # Dogs Get
    Animals_DogsGet_Params:
      type: object
      properties:
        type:
          type: integer
    Animals_DogsGet:
      type: object
      properties:
        name:
          type: string
        age:
          type: integer
    # Dogs Post
    Animals_DogsPost_Params:
      type: object
      properties:
        name:
          type: string
        age:
          type: integer
    Animals_DogsPost:
      type: object
      properties:
        result: boolean
  • スキーマーオブジェクトには多様なプロパティがあり、表現の自由度も高いため、フロントエンド・バックエンドで最低限必要な項目のみに絞るようにしています。
フィールドタイプ名 説明
type タイプ
required 必須
properties.type プロパティの型
properties.description プロパティの概要
properties.nullable
properties.enum

共通コンポーネント

./src/components/*.yaml

複数サービスで共通化する必要がある、抽象度の高いオブジェクトをコンポーネントとして記述します。

  • ( ファイル名に違和感があるのですが、refs参照の際、ファイル名がそのままオブジェクト名として生成されるため、キャメルケースとしています。)
Common_Image.yaml
type: object
description: |
  画像オブジェクト
properties:
  src:
    type: string
  alt:
    type: string
required:
  - src
  - alt

Exampleファイル

./src/services/{サービス名}/examples/*.yaml

Exampleをスキーマー定義と同一ファイルにおくと、見通しが悪くなるため、examplesディレクトリを区切り管理します。

  • (補足として、各スキーマーのexampleフォーマットは、openapi-generatorで、中間ファイルからopenapi.yamlを生成する際に、自動生成されるので、それを用いるとスムーズです。)
animals-dogs-example-1.yaml
value:
  dogs: [
    {
      id: 1,
      name: taro
    }
  ]

スクリプト例

主要な部分のみ抜粋してます。

1. 中間ファイル → openapi.yaml

root2openapi.sh
service_name=$1
root=${PWD}
src=${root}/src/services/${service_name}
out=${root}/openapi/${service_name}
components=${root}/src/components

docker run --rm \
  -v "${src}:/local/src/" \
  -v "${out}:/local/dist/openapi" \
  -v "${components}:/local/src/components" \
  openapitools/openapi-generator-cli generate \
    -g openapi-yaml \
    -i /local/src/root.yaml \
    -o /local/dist

2. openapi.yaml → rubyシリアライザ

openapi2generator-ruby.sh
service_name=$1
root=${PWD}
src=${root}/openapi/${service_name}/openapi.yaml
out=${root}/dist/openapi2generator-ruby

docker run --rm \
  -v "${src}:/local/openapi.yaml" \
  -v "${out}:/local/dist/openapi2generator-ruby" \
  openapitools/openapi-generator-cli generate \
    -g ruby \
    -i /local/openapi.yaml \
    -o /local/dist/openapi2generator-ruby

3. Swagger-ui起動

service_name=$1
root=${PWD}
openapi=${root}/openapi/${service_name}/

docker run \
  -p 80:8080 \
  -e SWAGGER_JSON=/src/openapi.yaml \
  -v `pwd`/openapi/${service_name}:/src swaggerapi/swagger-ui

余談:REST/RPCについて

本題から少し逸れますが、記事の例文では、わかりやすくするためにREST指向のエンドポイントで記述していますが、実際の運用では、REST/RCPの両方を許容する形で運用しています(既存のAPIがRESTというのもありますが)。ただ、混在させると困惑が生じるため、サービス・タグごとにAPI設計する中で、最適な方を採用するという方針としてます。

REST/RPCに関しては、OpenAPIを色々調査する中で、「OpenAPI(旧Swagger)はREST APIを設計するためのツール」と紹介される記事を多く目にしますが、OpenAPI 3.1.0では、下記のように「REST APIs」の表記が全て「HTTP APIs」と書き換わっていることは着目しておく必要はあるかなと思いました。

OpenAPI-Specification | OpenAPI supports any type of plain HTTP API

- language-agnostic interface description for REST APIs
+ language-agnostic interface description for HTTP APIs

またその中で、stoplightの開発者でもあるphilsturgeon氏が、下記のようにRPCについて言及しており、OpenAPI Initiativeメンバーであるdarrelmiller氏がそれに同意し、v4でのgRPCサポートも示唆されています。

● philsturgeon commented on Jun 12, 2019
Twice in the last few days I have had people ask if its ok to use OpenAPI for RPC, and I would say its better at describing RPC than REST currently.
ここ数日で2回、RPCにOpenAPIを使ってもいいかと聞かれたことがあります。現在のところ REST よりも RPC の記述の方が優れていると言っています。

Lets remove the limitation by fixing this wording, which would unblock larger talks about things like gRPC support for v4, and maybe even other level 0 implementations like GraphQL.
この文言を修正することで制限を取り除き、v4のためのgRPCサポートのようなものについての大きな話をブロックしないようにしましょう。

● darrelmiller commented on Jun 13, 2019
I do agree that attempting to associate OpenAPI to REST is no longer doing OpenAPI any favours.
OpenAPIをRESTに関連づけようとすることは、もはやOpenAPIのためにならないことに同意します。

誤解のないように補足しておくと、ここで言いたいこととしては、REST/RPCのどちらかが優れているのかという話ではなく、サービスの特性に合わせて、最適なAPIを設計をできるように、多くの可能性を選択肢として判断できるようにしておくことが大切だと思いました。

git. openapi-skeleton

今回紹介させていただいたYAMLファイルやスクリプトと置いてあります。

https://github.com/rkumagai/openapi-skeleton

まとめ

OpenAPIの運用に関しては、まだ導入フェーズということもあり、まだ詰め切れてないこともあり、運用しながら試行錯誤しながらブラッシュアップしていく予定です。また、OpenAPI自体の構成・管理方法よりも、実際にどうのようにAPIを設計するのかを考える方が重要で、難しいなと感じています。少しでも参考になれば幸いです。

明日はyamanokuさん記事になります。お楽しみに。

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

openapi-generatorのadditional-propertiesを指定して生成されるコードをカスタマイズしよう

https://github.com/OpenAPITools/openapi-generator

openapi-generatorはOpenAPIの定義ファイルから、API ClientやServer stubsなどを自動生成してくれる便利なものです。対応言語も豊富なので、これからAPI Clientを作ろうと考えている方は、これを用いて自動生成することをおすすめします。

$ openapi-generator generate -i ./openapi.yml -o ./openapi_client -g ruby

上記のコマンドを実行すると、openapi.ymlをもとに、openapi_clientというディレクトリ配下にrubyのコード(実態はgem)が生成されます。

openapi-generatorコマンドには多彩なオプションがあり、以下のコマンドで確認ができます。

$ openapi-generator help generate

本記事ではこの中にある additonal-properties というオプションについて説明します。

additonal-propertiesについて

このオプションは、生成される各言語ごとのオプションを設定できるものです。

https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/ruby.md

例えばRubyのAPI Clientであれば上記のドキュメントにあるものがすべてadditional-propertiesで指定できます。
RubyのAPI Clientはデフォルトでは openapi_client というgem名で生成されますが、このgemの名前を変えたい場合は下記のようにオプションを指定することで変更ができます。

$ openapi-generator generate -i ./openapi.yml -o ./openapi_client -g ruby --additional-properties=gemName=qiita_client

最近だと1つのリポジトリから複数のサービスのAPIを呼び出すことも多々あるでしょう。その場合はopenapiの定義ファイルが複数あることになると思いますが、すべて同じopenapi_clientというgemで生成されてしまうとどれか一つのサービスのAPIしか呼び出せなくて困ってしまいます。
そこで上記のオプションを使って別のgemとして生成することで、複数のサービスのAPIを呼び出すことが可能になります。
他にも便利そうなオプションがいくつかありますが、additional-propertisに複数指定したい場合はカンマ区切りでkeyとvalueをつなげると良いようです。詳しくはドキュメントを見てください。

言語ごとにオプションがいろいろあるので、すでに自動生成している方も一度ドキュメントを確認してみると良いのではないでしょうか?
それでは良い自動生成生活を!

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

gemのバージョン指定について

railsアプリケーションを作成するにあたって
gemファイル内のバージョン指定について理解が不十分だったので整理してみました。

Gemfileとは

・これは何?
 Bundlerというrubyのライブラリ管理システムのファイル。
・何ができる?
 railsアプリで使用するライブラリを管理することができる。

バージョンの基本の書き方

Gemfile
gem 'gem名', 'バージョン', 'オプション'

x.y.zの表記の意味

x:メジャーバージョン
 重大な変更。新機能の追加や、多くのAPI変更が含まれる。
y:マイナーバージョン
 新機能の追加やAPIの追加が含まれることがある。
z:パッチバージョン
 バグの修正が含まれる。

バージョン指定の指定について

固定

'x.y.z'

Gemfile
gem 'sqlite3', '1.3.6'

〜以上

'>=x.y.z'

Gemfile
gem 'sqlite3', '>=1.3.6'

x.x.x以上、x.y+1.0未満(メジャーアップデート不可)

'~>x.y.z'

Gemfile
gem 'sqlite3', '~>1.3.6'

以下と同義ですね。

Gemfile
gem 'sqlite3', '>=1.3.6', '<1.4.0'

x.y.z以降で最新のもの

'>=x.x.x'

Gemfile
gem 'sqlite3', '>=0.8.5'

参考

https://blog.yuhiisk.com/archive/2017/04/24/specify-the-version-of-gemfile.html
https://haayaaa.hatenablog.com/entry/2018/10/29/235952

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

DeviseのUserテーブルにUpdateアクションでカラムを更新しようとするとうまくいかない現象について

事の発端

Deviseで作ったUserモデルのテーブルにカラムを更新しようとするとできなかったことが始まりです。

意外なところで詰まったなぁと思ったので健忘録としてまとめます。

user_controller.rb
  def update
    @user = User.find(params[:id])
    if @user.update(user_params)
      redirect_to user_path(@user.id)
    else  
      render :show
    end
  end

僕は月の走行距離をマイページにて追加したかった為詳細ページにform_withを構えています。

show.html.erb
  <%= form_with(model: @user, local: true, class: "goal-form")  do |f| %>
    <%= f.text_field :distance, placeholder: "目標を記入する", class: "form__text" %>
    <%= f.submit "設定する", class: "btn btn-primary" %>
  <% end %>

送られてくる値も間違ってなかったのでなんでやねんと思っていたとこでした。
そこで以下のエラーを見つけました。
EFE24E99-97AA-473B-8332-1DEF840AD209_4_5005_c.jpeg

そもそもUserテーブルの編集にはPasswordの入力が必要だということ。

知りませんでした。
これまでユーザーの編集を行うことがなかったんです。

そこでPasswordを入力せずにユーザーの編集を行う方法を見つけました。
カラムを更新するには新たなコントローラの作成やメソッドが必要なようです。

まずは、users/registrations_controller.rbを作成します。

registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_account_update_params, only: [:update]

  protected

  def configure_account_update_params
    devise_parameter_sanitizer.permit(:account_update, keys: [:name])
  end
end

次にルーティングを設定します。

route.rb
  devise_for :users, controllers: {
    registrations: 'users/registrations'
  }

Userモデルを編集。メソッドを追加します。

user.rb
  メソッドを追加
  def update_without_current_password(params, *options)
    params.delete(:current_password)

    if params[:password].blank? && params[:password_confirmation].blank?
      params.delete(:password)
      params.delete(:password_confirmation)
    end

    result = update_attributes(params, *options)
    clean_up_passwords
    result
  end
end

Userモデルで定義したメソッドを呼び出します。

registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_account_update_params, only: [:update]

  protected
追加
  def update_resource(resource, params)
    resource.update_without_password(params)
  end

  def configure_account_update_params
    devise_parameter_sanitizer.permit(:account_update, keys: [:distance])
  end

また、モデルにpasswordのバリデーションがかかっているとまだエラーになると思うので外しておきましょう。

user.rb
  with_options presence: true do
    validates :nickname
    validates :email
    validates :password    ←消す
  end  

https://gyazo.com/d2738db841e41a0679d16fae6836ace9

できました。ほぼコピペです…

ここで初見だった方々が多かったのでまとめてみました。

・:account_update・・・Updateをするときに指定する引数。
・blank?・・・中身が空もしくは存在しないときにtrueを返す。
・update_attributes・・・一つのカラムのみを変更できる。しかし、バリデーションがスルーされる為、エラーの判定位が出ない。(これに苦しめられた)

キリがないのでここまで。

完成

これから、jsで今日走った距離が引かれて減っていくような機能を導入したいなと思います。
お疲れ様でした。

参考文献

https://qiita.com/j-sunaga/items/8d6769dfd04da5d3eed5

https://qiita.com/somewhatgood@github/items/b74107480ee3821784e6

https://pikawaka.com/rails/update

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

【メモ】backgroundの使い方

background

背景(画像)は以下の設定で行う

sample.html.erb
<body>
  <div class="top_wallpaper">
|
|
|
sample.css
.top_wallpaper{
  background-image: url(/wallpaper-new.jpg);
}

【CSS】
・該当するクラスに対し"background-image"を使用

書き方

background-image: url(/xxx.yy)
 ・/:フォルダの場所("/"のみの場合publicフォルダが自動的に適用)
 ・xxx:ファイル名
 ・yy:拡張子名

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