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

SDK for RubyでAWSの環境作ってみるよハンズオン

概要

コードでAWS環境を構築する、いわゆるInfrastructure as Code(Iac)と言えばCloudFormationTerraformが主流ですが、主要なプログラミング言語でリソースの構築や操作ができるSDKがAWSからは提供されています。
そのSDKのうち今回はRubyを使って環境(VPC、IGW、Subnet、RouteTable)を構築するハンズオンを紹介したいと思います!
※作業環境はMacOS、SDK for Rubyのバージョン3になります

おしながき

  1. 事前準備
  2. SDKのインストールと設定
  3. コードを書いていく
  4. どうやってコードを書いているのか?
  5. 今後
  6. まとめ

1.事前準備

必要なもの

  • AWSアカウント
    いわずもがなですが、各リソースを起動させるためにAWSのアカウントが必要です。もう持ってるという前提で進めますのでご了承

  • Ruby(バージョン1.9以降)
    これもいわずもがな…Rubyでコードを書いていきますので作業端末にインストールしてください。

参考サイト

AWS SDK for Ruby Developer Guide
こちらの公式サイトにそってやっていきます。

2.SDKのインストールと設定

SDKをインストールする

SDK for Rubyはgemとして配布されています。Rubyの外部ライブラリですね。

  • bundlerを使用している場合
    Gemfileに下記を追記してbundle installを実行する
gem 'aws-sdk'
  • bundler使わない場合
    下記のコマンドを実行してgemをインストール
sudo gem install aws-sdk

※けっこう時間がかかります。5分くらいターミナル画面が止まったままになりますが、フリーズではありません。

認証情報の設定

SDK経由でAWSにアクセスするための認証情報をローカルファイルに記述します。場所は~/.aws/credentialsです。

# ディレクトリを作成
mkdir ~/.aws

# 移動
cd ~/.aws

# vimでcredentialsファイルを編集
sudo vim credentials

vimで下記を記述します

[default]
aws_access_key_id = your_access_key_id
aws_secret_access_key = your_secret_access_key

※上記のyour_access_key_idyour_secret_access_keyにアクセスキー、シークレットアクセスキーを入力するため、一旦おいといて下の作業を進めます

アクセスキー、シークレットアクセスキーを作成する

  1. AWSマネジメントコンソールにログインします
  2. マネジメントコンソールの右上のアカウント部分をクリック
  3. ドロップダウンメニューから「マイセキュリティ資格情報」を選択
    スクリーンショット 2020-02-22 16.22.08.png
  4. 「セキュリティ認証情報」画面に移動したら「アクセスキーの作成」ボタンを押す
    ※アクセスキーは1アカウントにつき2つまでしか作成できません。既に2つある場合は使っていないものを削除するなどしてください
    スクリーンショット 2020-02-22 10.58.28.png
  5. 「アクセスキーの作成」ボタンを押して出てきたウインドウの情報をメモする
    ※このウインドウは一旦閉じると2度とシークレットアクセスキーを見られなくなるので注意!もし閉じた場合は、作成したキーを削除してもう一度作成しなおしてください
    ※画像のアクセスキーは削除してるので使えません あしからず(笑)
    スクリーンショット 2020-02-22 10.57.52.png
  6. vimに戻ってメモしたアクセスキーとシークレットアクセスキーの情報をそれぞれコピペする
  7. escキー => :wqで編集内容を保存してvimを閉じる

環境変数でリージョンを設定する

東京リージョンを使用するよう設定します

export AWS_REGION=ap-northeast-1

3.コードを書いていく

今回のゴールはVPCサブネットインターネットゲートウェイルートテーブルを作るところまでです。出来上がったrbファイルを実行するだけでAWSリソースが立ち上がる!というハンディさを実感してもらうのが目的なのでひとまずここまでで。

# sdkをrubyファイルに読み込み
require 'aws-sdk'

# VPCなどはEC2のカテゴリなのでAws::EC2::Clientクラスからインスタンスを作成
client = Aws::EC2::Client.new(region: "ap-northeast-1")


# VPC ----------------------------------------------------------------
# VPCを作成 上で作成したclientインスタンスに対してcreate_vpc()メソッドを適用する
resp_vpc = client.create_vpc({
  cidr_block: "10.0.0.0/16", # 必須項目 IPアドレス範囲を指定する
})

# 出来上がったVPCのIDを取得(他のリソース作成時やアタッチする時に使用する)
VPC_ID = resp_vpc.vpc.vpc_id

puts VPC_ID


# IGW ----------------------------------------------------------------
# インターネットゲートウェイ(IGW)を作成
resp_igw = client.create_internet_gateway({
})

# IGWのID
IGW_ID = resp_igw.internet_gateway.internet_gateway_id

# IGWをVPCにアタッチ
client.attach_internet_gateway({
  internet_gateway_id: IGW_ID, # 上で作成したIGWのID 
  vpc_id: VPC_ID, # 上で作成したVPCのID 
})

puts IGW_ID


# Subnet ----------------------------------------------------------------
# サブネットを作成(インターネット向けパブリックサブネット)
resp_pubsub1 = client.create_subnet({
  availability_zone: "ap-northeast-1a",
  cidr_block: "10.0.0.0/24", # 必須項目
  vpc_id: VPC_ID, # 必須項目
})

# サブネットのID
SUB_ID1 = resp_pubsub1.subnet.subnet_id

puts SUB_ID1


# RouteTable ----------------------------------------------------------------
# ルートテーブルを作成
resp_rt = client.create_route_table({
  vpc_id: VPC_ID, # 必須項目
})

# ルートテーブルのID
RT_ID = resp_rt.route_table.route_table_id

# ルートを作成(IGWに向けたルート)
resp_route = client.create_route({
  destination_cidr_block: "0.0.0.0/0", # インターネット向けルート
  gateway_id: IGW_ID, # IGWのID
  route_table_id: RT_ID, # 必須項目 ルートテーブルのID
})

# ルートテーブルをサブネットに紐付け
client.associate_route_table({
  route_table_id: RT_ID, # 必須項目 ルートテーブルのID
  subnet_id: SUB_ID1, # 紐付けるサブネットのID
})

puts RT_ID


# まとめてリソースにタグ(名前)を追加 ----------------------------------------------------------------
client.create_tags({
  resources: [VPC_ID, IGW_ID, SUB_ID1, RT_ID], # 必須項目 名前をつけるリソースのID
  tags: [
    {
      key: 'Name',
      value: 'HogeTestVPC', # VPCの名前
    },
    {
      key: 'Name',
      value: 'HogeTestIGW', # IGWの名前
    },
    {
      key: 'Name',
      value: 'HogeTestPublicSubnet1a', # Subnetの名前
    },
    {
      key: 'Name',
      value: 'HogeTestPublicRT', # RTの名前
    },
  ],
})

puts "Create environment successfully done!"

適当なディレクトリに上記のコードを.rbファイルとして保存して、cdコマンドで保存したディレクトリに移動。ruby 保存したファイル名.rbで実行するとリソースの作成がAWS上で始まります。

4.どうやってコードを書いているのか?

ひたすら下記の公式ドキュメントから該当するコードを引っ張ってきて、必要な要素を記述するだけです。ここが一番のツボというか、どのリソースに対して何をしたいのか?というのを考えてドキュメントから探し出すことさえ出来れば(書き方の良し悪しは置いておいて)リソースを操作するためのコードが書けるようになるはずです。

AWS SDK for Ruby V3

5.今後

公式ドキュメントの読み方や、EC2やその他のリソースに関する記述方法なども記事にしていけたらと考えています。また、今回はリソースを作成するだけでしたが、SDKの本領はプログラマブルなところで、書き方次第でAWSリソースを思い通りに操作することもできるみたいなので、そのへんも研究して記事にできたらいいなぁ、と思っています。

6.まとめ

ボク自身、ぜんぜんRubyやAWSに関しては初心者をようやく脱したかな?というレベルで、業務でSDK for Rubyを使うことになりました。思いの外、日本語での記事が少なく、四苦八苦しながら英語ドキュメントと戦って書いたので、同じようにSDKを使い始めようかという方の一助になればと思い記事を書きました。
また、SDKが提供されている言語を学習している方であれば、Gitでバージョン管理しつつGitHubに上げてポートフォリオとしてもいいかもな〜とか思いました。(CFnでもTerraformでもなくなぜSDK?ってツッコミには答えられるようにしたほうがいいかもですが笑…個人的にはSDKの方が学習コスト低いし、柔軟なリソース操作ができる可能性を感じます。)

最後に、掲載したコードはちゃんと走ることを確認していますが「書き方がなってない、もっとうまい書き方があるぞ」というコメントやご指摘などあれば、お手柔らかにお願いします(>人<;)

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

[Rails] ActiveRecordでデータを更新したときに、各カラムが更新されたかどうか知りたかったんだ

はじめに

タイトル通りです。

下のアプリはチャットルームに「タイトル」と「ルームの説明」、「カテゴリ」を設定できるんだけれども、
「タイトル」が変更された時だけトークルームに通知が来るようにしたかった。
Image from Gyazo

変更前のパラメーターを取得するか、変更されたかどうかを判定するメソッドがあったら嬉しいなーと思って探した。

実装

どうやらActiveRecordさんには「カラム名_changed?」っていうヘルパーメソッドがあるらしいと聞いて...

room_controller
  def update
    if @room.update(room_update_params)
      if @room.title_changed? # ココ。title_cahged?がtrueであれば自動メッセージを送るようにしたつもり。
        Message.create(room_id: @room.id, user_id: current_user.id, content: "*自動投稿* タイトルを「#{@room.title}」に編集しました")
      end
      redirect_to room_path(@room.id)
    else
      render :edit
    end
  end

こんな感じで実装してみたのだけれども失敗した。

Githubのrailsを見てみると、なんか更新されているらしい。

つまり

カラム名_changed?は、ActiveRecordのコールバックを使って処理を分けることができていたが、

コールバックまとめ

save前の場合は、

オブジェクト.will_save_change_to_カラム名

save後の場合は、

オブジェクト.saved_change_to_{カラム名}?

という風に書き変える必要があるみたい。

再度実装

room_controller.rb
  def update
    if @room.update(room_update_params)
      if @room.title_updated?
        Message.create(room_id: @room.id, user_id: current_user.id, content: "*自動投稿* タイトルを「#{@room.title}」に編集しました")
      end
      redirect_to room_path(@room.id)
    else
      render :edit
    end
  end

コントローラーで「saved_change_to」やら「will_save_change_to」やらを呼び出すとうまくいかなかったので、
モデル側に「更新されたかどうか判別するインスタンスメソッド」を作りました。

room.rb
  def title_updated?
    if self.saved_change_to_title?
      return true
    else
      return false
    end
  end

これでタイトル編集時のみメッセージが投稿されるようになりました。

Image from Gyazo

おわりに

今回初めてrailsのソースコードを見ましたが、もっと見ていくととても勉強になりそうです。
暇を見つけてもっと読んでみようと思いました。

おわり。

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

[取り置き機能]Rails初学者が某フリマアプリにオリジナル機能を実装してみた。

某スクールのチーム開発にて某フリマアプリを作成しております。
先日、必須機能の実装が完了し、追加機能を実装していくフェーズになりました。
そこで、自分なりに色々と考えた結果、タイトルに記載いたしました「取り置き機能」を実装しました!
今回は、機能追加を決めるにあたっての背景含めて以下に記載いたします。

今までは既存のコンテンツの実装であったため、様々な記事や情報があったため、自分で実装コンテンツや実装方法を考えるという機会は少し少なかった印象ありました。
しかしながら、今回はオリジナル機能であったため、実際のアプリユーザのことを考えながら実装をしていくのは、非常に楽しかったです!!

背景(「取り置き機能」を追加機能に選んだ理由)

今回、追加機能を選ぶにあたり、ネットで某フリマアプリユーザのコメントをネットで探したり、自分でサイトをいじっていました。
その結果、以下の状況が見えてきたため、今回の「取り置き機能」実装を決断しました。
- 問題・課題:
特定のユーザに販売する際に、出品写真やタイトルなどに”〇〇様向け”などと書くしかできず、他ユーザに購入できないようにするブロックができない。
その状況のため、別ユーザがユーザ名を偽ることで、本来購入をする予定であったユーザが購入できず、別のユーザに購入されてしまうという問題が発生している。

「取り置き機能」実装に関する考え方

今回の機能実装に関して、以下のように考えました。
1. 商品を出品するユーザが、商品に対して”キー”を設定する。
2. その”キー”を持っているユーザは購入をできるが、”キー”を持っていないユーザは購入をできないようにする。

ここで問題となったのは何を"キー”とするかでした。
「出品するユーザが合言葉を設定し、それを入力させる」や「ユーザ名を”キー”とする」などを考えましたが、どれもユニーク性がなかったためボツとし、今回は購入して欲しいユーザのemailアドレスを”キー”とすることにし,”reservation_email”というカラムを追加することにしました。
"reservation_email"の情報によって、入力された情報とログインしているユーザの情報が一致すれば、購入できる/一致しなければ、購入できないようにします。

*実際はemailアドレスを"キー”とするのはプライバシーの観点からあまりよろしくはないと思われますため、可能で有れば、会員番号などを”キー”とするのがベターかと考えます。(今回は会員番号を設定していなかったため、アドレスにしました)

実装内容(イメージ)

今回実装した機能のイメージは以下の通りになってます。

出品ユーザ側機能

機能1:取り置きをする

demo

機能2:取り置きをやめる

demo

機能3:その他ユーザに購入できないようにする

demo

購入ユーザ側機能

機能1:取置き品を購入する

demo
*購入すると以下のように、SOLDとなります。
demo
購入についての実装は、以下の記事をご参照ください。
[HowTo]Pay.jpを用いた商品購入機能実装から商品購入後の設定まで
https://qiita.com/Tatsu88/items/eb420e372077939a4627#%E5%95%86%E5%93%81%E8%B3%BC%E5%85%A5%E7%A2%BA%E8%AA%8D%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%AE%E7%B7%A8%E9%9B%86

機能実装:マイグレーションファイル

マイグレーションファイルは以下のように実装してます。
今回は、t.string :reservation_emailを追加してます。
こちらが、取り置き機能を実現するための要となります。
"reservation_email"の情報によって、入力された情報とログインしているユーザの情報が一致すれば、購入できる/一致しなければ、購入できないようにします。

マイグレーションファイル
class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    create_table :products do |t|
      t.string :name, limit: 191,null:false,index: true
      t.integer :price, index: true
      t.text :explain,null:false
      t.integer :postage,null:false
      t.integer :status
      t.integer :shipping_date
      t.integer :size
      t.integer :brand_id
      t.integer :category_id
      t.integer :prefecture
      t.integer :buyer_id
      t.references :user,index: true, foreign_key: true
      t.string :reservation_email
      t.timestamps
    end
  end
end

機能実装(ルーティング)

ルーティングは以下のように実装してます。
'reserve'、'reserved',patch 'reserve_cancel'を今回追加してます。
内容としては、予約・予約完了・予約取り消しとなります。

ルーティング
  resources :products do
    member do
      post 'purchase'
      get 'purchased'
      get 'buy'
      get 'reserve'
      patch 'reserved'
      patch 'reserve_cancel'
    end
    resources :comments,only:[:create,:destroy]
  end

機能実装(ビュー)

ビューは以下のように実装してます。
基本的に"reservation_email"の情報有無によって表示内容を変えてます。

1. 商品一覧

こちらでは購入済みか取り置き済みかでサムネイルとして表示する写真にラベルを取り付けます。

view
.product__thumbnail--image
 =image_tag (product.images[0].product_image.url)

#購入されていれば、"SOLD"のラベルをつけます。
-if product.buyer_id.present? 
 .items-box_photo__sold
  .items-box_photo__sold__inner SOLD

#取り置きされていれば、"Reserved"のラベルをつけます。
-if product.reservation_email.present? 
 .items-box_photo__reserved
  .items-box_photo__reserved__inner Reserved

2. 商品詳細

こちらではユーザの状態によって、ビューの表示内容を変えてます。

view
.product-buy__btn__box  
#ログインユーザが出品者の場合の、表示内容 
 - if user_signed_in? && current_user.id ==@product.user_id
  = link_to "削除する", product_path(@product.id), method: :delete,class:"product-details-delete__btn"
  = link_to "編集する", edit_product_path(@product.id),class:"product-details-edit__btn"
  = link_to "取り置きする/編集する", reserve_product_path(@product.id),class:"product-details-resorve__btn"

#購入者向けの表示内容
#既に購入されている時
 - elsif @product.buyer_id.present? 
  = link_to "売り切れました",buy_product_path,class:"disabled-button bold"

#取り置きされていて、その取り置きを商品を購入することを許可されているユーザの時
 - elsif @product.reservation_email.present? && @product.reservation_email == current_user.email
  = link_to "取り置き商品を購入する",buy_product_path,class:"product-purchase__btn"

#取り置きされていて、その取り置きを商品を購入することを許可されていないユーザの時
 - elsif @product.reservation_email.present? && @product.reservation_email != current_user.email
  = link_to "取り置き商品のため購入できません",buy_product_path,class:"disabled-button bold"
 - else
  = link_to "購入画面に進む",buy_product_path,class:"product-purchase__btn"

2. 取り置き画面

こちらでは、"reservation_email"というカラムの情報有無によって表示する内容を変えております。
情報が有れば、「取り消す」ボタンが出てきます。

ビュー(取り置き画面)
%main.buy-main 
  .buy-item-container
    %h2.buy-item-head 取り置き内容の確認
    %section.buy-content.buy-item
      .buy-content-inner
        .buy-item-box
          .buy-item-image
            =image_tag(@product.images[0].product_image.url,class:"buy-image")
          .buy-item-detail
            %p.buy-item-name
              =@product.name
              %p.buy-item-price.bold
                = number_to_currency(@product.price,format: "%u%n",unit:"¥",precision: 0)
                %span.item-shipping-fee.f14.bold
                  (税込)送料込み
        =form_for(@product, url: reserved_product_path,method: :patch) do |f|
          .form-group
            =f.label :お取り置きをする方のアドレス
            %span.form-group__require 必須
            %br/
            = f.email_field :reservation_email, {autofocus: true, autocomplete: "email", placeholder: "PC・携帯どちらでも可",class:'form-group__input'}
            = f.submit '取り置きする', class: "reserve"
        - if @product.reservation_email.present?
          =link_to reserve_cancel_product_path,method: :patch, class:"btn-default btn-red" do
            取り置きをやめる

機能実装について(コントローラ)

コントローラは以下のように実装してます。
ポイントは以下の通りになってます。
1. reservedアクションで入力された”reservation_email”をproductに追加します。
 *この”reservation_email”が取置き品を購入するためのキーとなります。
2. 取り置きをキャンセルする時と購入がされた後は”reservation_email”のvalueをなくします。

コントローラ(該当箇所のみ)
before_action :set_product, only: [:reserved,:reserve,:reserve_cancel,:purchase]

def reserve
end

def reserved  

#reservedアクションで入力された”reservation_email”をproductに追加します。
 @product.update(product_params)  
 if @product.reservation_email.present?
   else
     render :reserve
  end
end

def reserve_cancel

#”reservation_email”のvalueをなくします。
 if @product.update(reservation_email:"")
  redirect_to product_path
 else
  redirect_to product_path
  end
end

def purchase
  Payjp.api_key = Rails.application.secrets.payjp_access_key
  charge = Payjp::Charge.create(
    amount: @product.price,
    customer: Payjp::Customer.retrieve(@creditcard.customer_id),
    currency: 'jpy'
  )
#”reservation_email”がある場合は、valueをなくします。
  if @product.reservation_email.present?
    @product.update(reservation_email:"")
  end
  @product_buyer= Product.find(params[:id])
  @product_buyer.update( buyer_id: current_user.id)
  redirect_to purchased_product_path
end

private
def product_params
      params.require(:product).permit(:name,:category_id,:price,:explain,:size,:brand_id,:status,:postage,:shipping_date,:prefecture,:reservation_email,images_attributes: [:product_image,:_destroy,:id]).merge(user_id: current_user.id)
end

def set_product
  @product = Product.includes(:comments).find(params[:id])
end

参照

【Rails】updateメソッドの使い方を徹底解説!
https://pikawaka.com/rails/update

【Rails】form_forの使い方を徹底解説!
https://pikawaka.com/rails/form_for#form_for%E3%81%A7%E3%81%AE%E4%BF%9D%E5%AD%98%E6%96%B9%E6%B3%95%E3%81%AE%E6%B3%A8%E6%84%8F%E7%82%B9

Railsのモデルの作成、検索、更新、削除のよく使うメソッドのまとめ
https://ruby-rails.hatenadiary.com/entry/20140724/1406142120

以上となります。最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。

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

[取り置き機能]某フリマアプリにオリジナル機能を実装してみた。

某スクールのチーム開発にて某フリマアプリを作成しております。
先日、必須機能の実装が完了し、追加機能を実装していくフェーズになりました。
そこで、自分なりに色々と考えた結果、タイトルに記載いたしました「取り置き機能」を実装しました!
今回は、機能追加を決めるにあたっての背景含めて以下に記載いたします。

今までは既存のコンテンツの実装であったため、様々な記事や情報があったため、自分で実装コンテンツや実装方法を考えるという機会は少し少なかった印象ありました。
しかしながら、今回はオリジナル機能であったため、実際のアプリユーザのことを考えながら実装をしていくのは、非常に楽しかったです!!

より良いコードの書き方や修正すべき点などご意見ございましたら、是非いただけますと幸いです!

背景(「取り置き機能」を追加機能に選んだ理由)

今回、追加機能を選ぶにあたり、ネットで某フリマアプリユーザのコメントをネットで探したり、自分でサイトをいじっていました。
その結果、以下の状況が見えてきたため、今回の「取り置き機能」実装を決断しました。
- 問題・課題:
特定のユーザに販売する際に、出品写真やタイトルなどに”〇〇様向け”などと書くしかできず、他ユーザに購入できないようにするブロックができない。
その状況のため、別ユーザがユーザ名を偽ることで、本来購入をする予定であったユーザが購入できず、別のユーザに購入されてしまうという問題が発生している。

「取り置き機能」実装に関する考え方

今回の機能実装に関して、以下のように考えました。
1. 商品を出品するユーザが、商品に対して”キー”を設定する。
2. その”キー”を持っているユーザは購入をできるが、”キー”を持っていないユーザは購入をできないようにする。

ここで問題となったのは何を"キー”とするかでした。
「出品するユーザが合言葉を設定し、それを入力させる」や「ユーザ名を”キー”とする」などを考えましたが、どれもユニーク性がなかったためボツとし、今回は購入して欲しいユーザのemailアドレスを”キー”とすることにし,”reservation_email”というカラムを追加することにしました。
"reservation_email"の情報によって、入力された情報とログインしているユーザの情報が一致すれば、購入できる/一致しなければ、購入できないようにします。

*実際はemailアドレスを"キー”とするのはプライバシーの観点からあまりよろしくはないと思われますため、可能で有れば、会員番号などを”キー”とするのがベターかと考えます。(今回は会員番号を設定していなかったため、アドレスにしました)

実装内容(イメージ)

今回実装した機能のイメージは以下の通りになってます。

出品ユーザ側機能

機能1:取り置きをする

demo

機能2:取り置きをやめる

demo

機能3:その他ユーザに購入できないようにする

demo

購入ユーザ側機能

機能1:取置き品を購入する

demo
*購入すると以下のように、SOLDとなります。
demo
購入についての実装は、以下の記事をご参照ください。
[HowTo]Pay.jpを用いた商品購入機能実装から商品購入後の設定まで
https://qiita.com/Tatsu88/items/eb420e372077939a4627#%E5%95%86%E5%93%81%E8%B3%BC%E5%85%A5%E7%A2%BA%E8%AA%8D%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%AE%E7%B7%A8%E9%9B%86

機能実装:マイグレーションファイル

マイグレーションファイルは以下のように実装してます。
今回は、t.string :reservation_emailを追加してます。
こちらが、取り置き機能を実現するための要となります。
"reservation_email"の情報によって、入力された情報とログインしているユーザの情報が一致すれば、購入できる/一致しなければ、購入できないようにします。

マイグレーションファイル
class CreateProducts < ActiveRecord::Migration[5.0]
  def change
    create_table :products do |t|
      t.string :name, limit: 191,null:false,index: true
      t.integer :price, index: true
      t.text :explain,null:false
      t.integer :postage,null:false
      t.integer :status
      t.integer :shipping_date
      t.integer :size
      t.integer :brand_id
      t.integer :category_id
      t.integer :prefecture
      t.integer :buyer_id
      t.references :user,index: true, foreign_key: true
      t.string :reservation_email
      t.timestamps
    end
  end
end

機能実装:ルーティング

ルーティングは以下のように実装してます。
'reserve'、'reserved',patch 'reserve_cancel'を今回追加してます。
内容としては、予約・予約完了・予約取り消しとなります。

ルーティング
  resources :products do
    member do
      post 'purchase'
      get 'purchased'
      get 'buy'
      get 'reserve'
      patch 'reserved'
      patch 'reserve_cancel'
    end
    resources :comments,only:[:create,:destroy]
  end

機能実装:ビュー

ビューは以下のように実装してます。
基本的に"reservation_email"の情報有無によって表示内容を変えてます。

1. 商品一覧

こちらでは購入済みか取り置き済みかでサムネイルとして表示する写真にラベルを取り付けます。

view
.product__thumbnail--image
 =image_tag (product.images[0].product_image.url)

#購入されていれば、"SOLD"のラベルをつけます。
-if product.buyer_id.present? 
 .items-box_photo__sold
  .items-box_photo__sold__inner SOLD

#取り置きされていれば、"Reserved"のラベルをつけます。
-if product.reservation_email.present? 
 .items-box_photo__reserved
  .items-box_photo__reserved__inner Reserved

2. 商品詳細

こちらではユーザの状態によって、ビューの表示内容を変えてます。

view
.product-buy__btn__box  
#ログインユーザが出品者の場合の、表示内容 
 - if user_signed_in? && current_user.id ==@product.user_id
  = link_to "削除する", product_path(@product.id), method: :delete,class:"product-details-delete__btn"
  = link_to "編集する", edit_product_path(@product.id),class:"product-details-edit__btn"
  = link_to "取り置きする/編集する", reserve_product_path(@product.id),class:"product-details-resorve__btn"

#購入者向けの表示内容
#既に購入されている時
 - elsif @product.buyer_id.present? 
  = link_to "売り切れました",buy_product_path,class:"disabled-button bold"

#取り置きされていて、その取り置きを商品を購入することを許可されているユーザの時
 - elsif @product.reservation_email.present? && @product.reservation_email == current_user.email
  = link_to "取り置き商品を購入する",buy_product_path,class:"product-purchase__btn"

#取り置きされていて、その取り置きを商品を購入することを許可されていないユーザの時
 - elsif @product.reservation_email.present? && @product.reservation_email != current_user.email
  = link_to "取り置き商品のため購入できません",buy_product_path,class:"disabled-button bold"
 - else
  = link_to "購入画面に進む",buy_product_path,class:"product-purchase__btn"

2. 取り置き画面

こちらでは、"reservation_email"というカラムの情報有無によって表示する内容を変えております。
情報が有れば、「取り消す」ボタンが出てきます。

ビュー(取り置き画面)
%main.buy-main 
  .buy-item-container
    %h2.buy-item-head 取り置き内容の確認
    %section.buy-content.buy-item
      .buy-content-inner
        .buy-item-box
          .buy-item-image
            =image_tag(@product.images[0].product_image.url,class:"buy-image")
          .buy-item-detail
            %p.buy-item-name
              =@product.name
              %p.buy-item-price.bold
                = number_to_currency(@product.price,format: "%u%n",unit:"¥",precision: 0)
                %span.item-shipping-fee.f14.bold
                  (税込)送料込み
        =form_for(@product, url: reserved_product_path,method: :patch) do |f|
          .form-group
            =f.label :お取り置きをする方のアドレス
            %span.form-group__require 必須
            %br/
            = f.email_field :reservation_email, {autofocus: true, autocomplete: "email", placeholder: "PC・携帯どちらでも可",class:'form-group__input'}
            = f.submit '取り置きする', class: "reserve"
        - if @product.reservation_email.present?
          =link_to reserve_cancel_product_path,method: :patch, class:"btn-default btn-red" do
            取り置きをやめる

機能実装:コントローラ

コントローラは以下のように実装してます。
ポイントは以下の通りになってます。
1. reservedアクションで入力された”reservation_email”をproductに追加します。
 *この”reservation_email”が取置き品を購入するためのキーとなります。
2. 取り置きをキャンセルする時と購入がされた後は”reservation_email”のvalueをなくします。

コントローラ(該当箇所のみ)
before_action :set_product, only: [:reserved,:reserve,:reserve_cancel,:purchase]

def reserve
end

def reserved  

#reservedアクションで入力された”reservation_email”をproductに追加します。
 @product.update(product_params)  
 if @product.reservation_email.present?
   else
     render :reserve
  end
end

def reserve_cancel

#”reservation_email”のvalueをなくします。
 if @product.update(reservation_email:"")
  redirect_to product_path
 else
  redirect_to product_path
  end
end

def purchase
  Payjp.api_key = Rails.application.secrets.payjp_access_key
  charge = Payjp::Charge.create(
    amount: @product.price,
    customer: Payjp::Customer.retrieve(@creditcard.customer_id),
    currency: 'jpy'
  )
#”reservation_email”がある場合は、valueをなくします。
  if @product.reservation_email.present?
    @product.update(reservation_email:"")
  end
  @product_buyer= Product.find(params[:id])
  @product_buyer.update( buyer_id: current_user.id)
  redirect_to purchased_product_path
end

private
def product_params
      params.require(:product).permit(:name,:category_id,:price,:explain,:size,:brand_id,:status,:postage,:shipping_date,:prefecture,:reservation_email,images_attributes: [:product_image,:_destroy,:id]).merge(user_id: current_user.id)
end

def set_product
  @product = Product.includes(:comments).find(params[:id])
end

参照

【Rails】updateメソッドの使い方を徹底解説!
https://pikawaka.com/rails/update

【Rails】form_forの使い方を徹底解説!
https://pikawaka.com/rails/form_for#form_for%E3%81%A7%E3%81%AE%E4%BF%9D%E5%AD%98%E6%96%B9%E6%B3%95%E3%81%AE%E6%B3%A8%E6%84%8F%E7%82%B9

Railsのモデルの作成、検索、更新、削除のよく使うメソッドのまとめ
https://ruby-rails.hatenadiary.com/entry/20140724/1406142120

以上となります。最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。

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

rails:ページ移行で、(navbarなど)リンク表示を変更する

いらないリンクを消したい時

ログインページにいるのにログインリンク。新規登録ページにいるのにsignup(新規登録)のリンクはいらないですよね。
僕も今回、学習段階で実装する場面が来たので簡単にご紹介します。

・新規登録ページ

navbarのリンクはlogin表示のみ。signup(新規登録)リンクは隠す
スクリーンショット 2020-02-22 19.25.25.png

・ログインページ

navbarのリンクはsignup(新規登録)表示のみ。loginリンクは隠す
スクリーンショット 2020-02-22 19.04.45.png

該当コード

request:ユーザのヘッダー情報や環境変数を取得

#新規登録画面におけるnavbarのコード

<% unless request.path.include?("login") %> #"login"とのurlを含まなければtrue
    <li class="nav-item"><%= link_to 'Log in', login_path, class:'nav-link' %></li>
<% end %>


#ログイン画面におけるnavbarのコード

<% unless request.path.include?("users/new") %> #"users/new"とのurlを含まなければtrue
    <li class="nav-item"><%= link_to 'Sign up', new_user_path, class:'nav-link'%></li>
<% end %>
#"signup"または"users/new"とのurlを含まなければtrue

<% unless request.path.include?("signup") || request.path.include?("users/new")%>
    <li class="nav-item"><%= link_to 'Sign up', new_user_path, class:'nav-link'%></li>
<% end %>

補足

足りない部分や、間違っている箇所、もっときれいにコードを書ける部分があればご指摘いただきたいです。

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

【Tips】Ubuntuへのmysql2 gemインストール時に発生しうるエラーへの対処

問題

Ruby on Rails 等で利用する Ruby の MySQL 用ライブラリmysql2インストールが失敗する。

入力

gem install mysql2

出力

Building native extensions. This could take a while...
ERROR:  Error installing mysql2:
        ERROR: Failed to build gem native extension.

    current directory: /home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/mysql2-0.5.3/ext/mysql2
/home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/bin/ruby -I /home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/lib/ruby/2.7.0 -r ./siteconf20200222-2823-u3a2hd.rb extconf.rb
checking for rb_absint_size()... yes
checking for rb_absint_singlebit_p()... yes
checking for rb_wait_for_single_fd()... yes
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
        --with-opt-dir
        --without-opt-dir
        --with-opt-include
        --without-opt-include=${opt-dir}/include
        --with-opt-lib
        --without-opt-lib=${opt-dir}/lib
        --with-make-prog
        --without-make-prog
        --srcdir=.
        --curdir
        --ruby=/home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/bin/$(RUBY_BASE_NAME)
        --with-mysql-dir
        --without-mysql-dir
        --with-mysql-include
        --without-mysql-include=${mysql-dir}/include
        --with-mysql-lib
        --without-mysql-lib=${mysql-dir}/lib
        --with-mysql-config
        --without-mysql-config
        --with-mysqlclient-dir
        --without-mysqlclient-dir
        --with-mysqlclient-include
        --without-mysqlclient-include=${mysqlclient-dir}/include
        --with-mysqlclient-lib
        --without-mysqlclient-lib=${mysqlclient-dir}/lib
        --with-mysqlclientlib
        --without-mysqlclientlib
/home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/lib/ruby/2.7.0/mkmf.rb:1050:in `block in find_library': undefined method `split' for nil:NilClass (NoMethodError)
        from /home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/lib/ruby/2.7.0/mkmf.rb:1050:in `collect'
        from /home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/lib/ruby/2.7.0/mkmf.rb:1050:in `find_library'
        from extconf.rb:87:in `<main>'

To see why this extension failed to compile, please check the mkmf.log which can be found here:

  /home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/extensions/x86_64-linux/2.7.0/mysql2-0.5.3/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in /home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/mysql2-0.5.3 for inspection.
Results logged to /home/【ユーザー名】/.anyenv/envs/rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/extensions/x86_64-linux/2.7.0/mysql2-0.5.3/gem_make.out

環境

  • Windows Subsystem for Linux (WSL)
    • Ubuntu 18.04 LTS
  • anyenv 1.1.1
    • rbenv 1.1.2-20-g143b2c9
      • ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-linux]

原因:MySQLの開発用ライブラリ不足

「Could not create Makefile due to some reason, probably lack of necessary libraries and/or headers.」とあるように、native extensionとして必要なMySQLに関連するライブラリがインストールされていない。

解決方法

入力

sudo apt install libmysqld-dev
gem install mysql2

出力

Building native extensions. This could take a while...
Successfully installed mysql2-0.5.3
Parsing documentation for mysql2-0.5.3
Installing ri documentation for mysql2-0.5.3
Done installing documentation for mysql2 after 0 seconds
1 gem installed

参考文献

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

Ruby(とC)でRubyを実装してみた(builtinで遊んでみた)

はじめに

タイトルにもあるようにRubyのbuiltin(正式名称を知らないので呼び出し方法から拝借)というものを使ってRuby自体をRuby(とC)で実装してみた話です。

内容としてはRuby自体の実装に興味のある方向けの話になります。

builtinって?

builtinとはRuby(とC)でRuby自体を実装するというものです(正式な名前は今のところないみたい?)。以下のように__builtin_<Cで定義した関数名>をRubyのコードから呼び出すことでRubyとCを使い、より簡単にRubyの実装を行うことができます。

たとえば、Hash#deleteはCで以下のように実装されています。

static VALUE
rb_hash_delete_m(VALUE hash, VALUE key)
{
    VALUE val;

    rb_hash_modify_check(hash);
    val = rb_hash_delete_entry(hash, key);

    if (val != Qundef) {
    return val;
    }
    else {
    if (rb_block_given_p()) {
        return rb_yield(key);
    }
    else {
        return Qnil;
    }
    }
}

第一引数のhashはハッシュ自体を引数に受け取り、第二引数のkeyHash#deleteで渡しているキーを受け取っています。ちなみに、Ruby側の変数などの値はVALUE型で受け取り、Cの関数で処理されています。

    rb_hash_modify_check(hash);

rb_hash_modify_check関数は内部でrb_check_frozen関数を実行し、ハッシュが凍結されているかを確認しています。

static void
rb_hash_modify_check(VALUE hash)
{
    rb_check_frozen(hash); // オブジェクトが凍結されているか確認
}

val = rb_hash_delete_entry(hash, key);では引数に受け取ったキーをもとに削除する値を取得し、同時に削除を行っています。キーと対になる値がない場合はQundefというCで使用する未定義の値が入ります。

    if (val != Qundef) {
    return val;
    }
    else {
    if (rb_block_given_p()) {
        return rb_yield(key);
    }
    else {
        return Qnil;
    }
    }

valの値で処理を分岐させ、Qundefではない場合(つまりキーを使って値が取れ、削除できた場合)は削除された値を返します。
Qundefだった場合はQnil(Rubyでのnil)を返します。ブロックが渡されている場合はrb_yield(key)を実行し、その結果を返しています。

このように皆さんが普段使っているRubyは、Cを使って実装されています。

builtin機能を使うことで先ほどのコードが以下のようになります。

class Hash
    def delete(key)
        value = __builtin_rb_hash_delete_m(key)

        if value.nil?
            if block_given?
                yield key
            else
                nil
            end
        else
            value
        end
    end
end
static VALUE
rb_hash_delete_m(rb_execution_context_t *ec, VALUE hash, VALUE key)
{
    VALUE val;

    rb_hash_modify_check(hash);
    val = rb_hash_delete_entry(hash, key);

    if (val != Qundef) {
        return val;
    }
    else {
        return Qnil;
    }
}

Ruby側でブロックの実行などを処理させているため。Cでの実装はよりシンプルで読みやすくなったと思います。
また

このようにbuiltin機能を使うことでRubyと少しのCのコードでRubyを実装することができます。

またCで実装するよりもRubyで実装した場合、パフォーマンスが向上するケースもあるようです。
より具体的な話は笹田さんがRubyKaigi 2019にて話されていますのでそちらを参照して頂ければと思います。

Write a Ruby interpreter in Ruby for Ruby 3

やってみた

builtinを使うことでRubyのコードを使い、メソッドを実装できることがわかったので実際にやってみました。

開発環境構築

まずはRubyの開発環境を作成するところからはじめました。僕の環境としてはWSL+Ubuntu 18.04を使い、開発環境を構築しました。
基本的な手順としてはRuby Hack Challengeの(2) MRI ソースコードの構造を参考に進めました。

まずは使用するライブラリなどをインストールしていきます。

sudo apt install git ruby autoconf bison gcc make zlib1g-dev libffi-dev libreadline-dev libgdbm-dev libssl-dev

次に、作業用のディレクトリを作成し、そこへ移動します。。

mkdir workdir
cd workdir

作業用のディレクトリに移動後、Rubyのソースコードをcloneします。結構時間がかかるのでこの間にコーヒーでも入れておくといいでしょう。

git clone https://github.com/ruby/ruby.git

ソースコードのcloneが終わったら、rubyディレクトリへと移動し、autoconfを実行します。あとで実行するconfigureスクリプトを生成するためですね。実行後、workdirまで戻ります。

cd ruby
autoconf
cd ..

次に、ビルド用のディレクトリを作成し、そこへ移動します。

mkdir build
cd build

../ruby/configure --prefix=$PWD/../install --enable-sharedを実行してビルドするためのMakefileを作成します。また--prefix=$PWD/../installではRubyをインストールする先を指定しています

../ruby/configure --prefix=$PWD/../install --enable-shared

その後、make -jを実行してビルドします。-jは並列にコンパイルを実行するためのオプションです。特に急ぐわけでもない場合はmakeだけでも良いでしょう。

make -j

最後にmake installを実行するとworkdirディレクトリ内にinstallディレクトリが作成され、Rubyがインストールされます。

make install

これで最新のRubyがworkdir/installにインストールされています。

ちなみに、本当にインストールされているか気になる方は../install/bin/ruby -vを実行してみましょう。ruby 2.8.0devとRubyのバージョンが表示されていればRubyは正しくインストールされています。

builtinでメソッドを再定義してみる

開発環境が整ったのでbuiltinを使い、メソッドを再定義していきます。先ほど例にも挙げたHash#deleteを再実装していきます。

common.mkの修正

まずは、ビルドの際にRubyのソースコードを使用するための諸設定をcommon.mkに追加します。
common.mkの1000行目辺りに、BUILTIN_RB_SRCSという記述があります。このBUILTIN_RB_SRCSで読み込むRubyのコードが記述されているファイルを追加します。

common.mk
BUILTIN_RB_SRCS = \
        $(srcdir)/ast.rb \
        $(srcdir)/gc.rb \
        $(srcdir)/io.rb \
        $(srcdir)/pack.rb \
        $(srcdir)/trace_point.rb \
        $(srcdir)/warning.rb \
        $(srcdir)/array.rb \
        $(srcdir)/prelude.rb \
        $(srcdir)/gem_prelude.rb \
        $(empty)
BUILTIN_RB_INCS = $(BUILTIN_RB_SRCS:.rb=.rbinc)

今回は、Hashの実装を行うためhash.rbを以下のように追加します。

BUILTIN_RB_SRCS = \
        $(srcdir)/ast.rb \
        $(srcdir)/gc.rb \
        $(srcdir)/io.rb \
        $(srcdir)/pack.rb \
        $(srcdir)/trace_point.rb \
        $(srcdir)/warning.rb \
        $(srcdir)/array.rb \
        $(srcdir)/prelude.rb \
        $(srcdir)/gem_prelude.rb \
+       $(srcdir)/hash.rb \
        $(empty)
BUILTIN_RB_INCS = $(BUILTIN_RB_SRCS:.rb=.rbinc)

次に、2520行目辺りにあるHashのビルドで読み込むファイルを指定している部分を修正します。
このようにhash.cなど読み込むファイルが指定されています。

common.mk
hash.$(OBJEXT): {$(VPATH)}hash.c
hash.$(OBJEXT): {$(VPATH)}id.h
hash.$(OBJEXT): {$(VPATH)}id_table.h
hash.$(OBJEXT): {$(VPATH)}intern.h
hash.$(OBJEXT): {$(VPATH)}internal.h
hash.$(OBJEXT): {$(VPATH)}missing.h

ここに、hash.rbincbuiltin.hを追加します。

hash.$(OBJEXT): {$(VPATH)}hash.c
+hash.$(OBJEXT): {$(VPATH)}hash.rbinc
+hash.$(OBJEXT): {$(VPATH)}builtin.h
hash.$(OBJEXT): {$(VPATH)}id.h
hash.$(OBJEXT): {$(VPATH)}id_table.h
hash.$(OBJEXT): {$(VPATH)}intern.h
hash.$(OBJEXT): {$(VPATH)}internal.h
hash.$(OBJEXT): {$(VPATH)}missing.h

hash.rbincmake実行時に自動的に生成されるファイルで、hash.rb内の__builtin_<呼び出すCの関数名>をチェックした内容をもとに生成されています。またbuiltin.hはbuiltinを使うための実装などがかかれたヘッダーファイルです。

これでcommon.mkでの修正は完了です。

inits.cの修正

次に、inits.cを修正します。といっても非常に修正は簡単なものです。

inits.c
#define BUILTIN(n) CALL(builtin_##n)
    BUILTIN(gc);
    BUILTIN(io);
    BUILTIN(ast);
    BUILTIN(trace_point);
    BUILTIN(pack);
    BUILTIN(warning);
    BUILTIN(array);
    Init_builtin_prelude();
}

inits.cでは上記のようにbuiltinを使用しているRubyのソースファイルを追加しています。ここに同じようにBUILTIN(hash);を追加します。

#define BUILTIN(n) CALL(builtin_##n)
    BUILTIN(gc);
    BUILTIN(io);
    BUILTIN(ast);
    BUILTIN(trace_point);
    BUILTIN(pack);
    BUILTIN(warning);
    BUILTIN(array);
+    BUILTIN(hash);
    Init_builtin_prelude();

inits.cの修正はこれでOKです。

hash.cの修正

いよいよ、hash.cのコードを修正していきます。

builtin.hの読み込み

まずは、40行目辺りのヘッダー読み込み部分に#include "builtin.h"を追加します。

  #include "ruby/st.h"
  #include "ruby/util.h"
  #include "ruby_assert.h"
  #include "symbol.h"
  #include "transient_heap.h"
+ #include "builtin.h"

これでbuiltinに必要な構造体などをhash.cで使用することができます。

Hash#deleteの定義を削除

次に、Hash#deleteを定義している部分を取り除きます。

hash.cの下部にInit_Hash(void)という関数が定義されていると思います。

void
Init_Hash(void)
{
 /// Hashの実装コードなどが記述されています。
}

Rubyの各クラスのメソッドはこの関数内で以下のように定義されています。

rb_define_method(rb_cHash, "delete", rb_hash_delete_m, 1);

rb_define_methodはRubyでいうところのメソッドの定義と同じと考えてください。第一引数にメソッドを定義するクラスのVALUEを渡し、第二引数がメソッド名となっています。
 第三引数がCで定義された関数(メソッドで実行される処理)で、第四引数がメソッドが受け取る引数の数となっています。

builtinでRubyのメソッドを定義する場合はこの定義部分を削除する必要があります。今回はHash#deleteを再実装しますので、deleteが定義されている部分を削除します。

    rb_define_method(rb_cHash, "shift", rb_hash_shift, 0);
-   rb_define_method(rb_cHash, "delete", rb_hash_delete_m, 1);
    rb_define_method(rb_cHash, "delete_if", rb_hash_delete_if, 0);

rb_hash_delete_mをbuiltinから使用できるように修正

先ほど削除したrb_define_method(rb_cHash, "delete", rb_hash_delete_m, 1);で呼び出されているrb_hash_delete_mをbuiltinで使用できるように修正します。

2380行辺りにrb_hash_delete_mの実装があります。

static VALUE
rb_hash_delete_m(VALUE hash, VALUE key)
{
    VALUE val;

    rb_hash_modify_check(hash);
    val = rb_hash_delete_entry(hash, key);

    if (val != Qundef) {
    return val;
    }
    else {
    if (rb_block_given_p()) {
        return rb_yield(key);
    }
    else {
        return Qnil;
    }
    }
}

これを以下のように修正します。

static VALUE
rb_hash_delete_m(rb_execution_context_t *ec, VALUE hash, VALUE key)
{
    VALUE val;

    rb_hash_modify_check(hash);
    val = rb_hash_delete_entry(hash, key);

    if (val != Qundef)
    {
        return val;
    }
    else
    {
        return Qnil;
    }
}

builtin対応のために第一借り引数にrb_execution_context_t *ecを渡しているところが実装の肝ですね。

これでRubyからCで定義した関数を呼び出すことができるようになります。

hash.rbincの読み込み

最後に、自動生成されるhash.rbincを読み込むようにします。
#include "hash.rbinc"hash.cの一番下に追加します。

#include "hash.rbinc"

これでCのコード側での修正は完了しました。

hash.rbの作成

それではRubyでHash#deleteを実装してみましょう。hash.cと同じ階層にhash.rbを作成します。
作成後、以下のようにコードを追加します。

class Hash
    def delete(key)
        puts "impl by Ruby(& C)!"
        value = __builtin_rb_hash_delete_m(key)

        if value.nil?
            if block_given?
                yield key
            else
                nil
            end
        else
            value
        end
    end
end

先ほどbuiltinで呼び出せるようにした__builtin_rb_hash_delete_mに受け取ってきた引数を渡し、その結果をvalueに代入しています。

あとは、valueの値がnilか同課で処理を分岐させています。nilの場合かるブロックが渡されている場合はkeyを引数にブロックを実行しています。

puts "impl by Ruby(& C)!"は実際に試す際に確認するためのメッセージになりますね。

これでbuiltinでの実装はすべて完了しました!

ビルドしてみる

それでは開発環境を構築した時同様にビルドしてみましょう。

make -j && make install

ビルドが成功すればOKです!もしビルドが失敗した場合はtypoなどが無いか確認してみましょう。

実際にirbで試してみる

それではirbを使ってbuiltinで実装したHash#deleteを試してみましょう!

../install/bin/irb

後は以下のコードを貼り付けてみましょう!

hash = {:key => "value"}
hash.delete(:k)
hash.delete(:key)

以下のように結果が表示されていればbuiltinでの実装は完了です!

irb(main):001:0> hash = {:key => "value"}
irb(main):002:0> hash.delete(:k)
impl by Ruby(& C)!
=> nil
irb(main):003:0> hash.delete(:key)
impl by Ruby(& C)!
=> "value"
irb(main):004:0>

impl by Ruby(& C)!と表示されているのでRubyで定義したHash#deleteが実行されていることが分かりますね。

これでRuby(とC)でRubyを実装できました!

終わりに

このようにbuiltinを使うことでRubyと(少しのC)のコードを使ってRuby自体を実装することができます。
そのため普段Rubyを書いている人でも気軽にメソッドの修正などのパッチを送ることができるようになるのではないかと思います。

あとやってみてRuby側で処理が書けたりするので意外と書きやすいというのはうれしいですね。

個人的にはExtensionなどでも使用できるようになるとC/C++でのRuby拡張がより書きやすくなるのではないかと思うので、今後の展望が非常に楽しみですね。

参考

Write a Ruby interpreter in Ruby for Ruby 3

Rubyソースコード完全解説

Rubyのしくみ

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

ユーザ情報をどうDB設計するか突き詰めて考えてみる

この記事について

  • システム開発するうえで絶対に避けては通れないユーザ情報周りの設計を自分なりに考察してみた
  • 結局何が最適かわからないけど色々と考え出すと奥が深かった

きっかけ的な

Railsでアプリ作ったんだけど、bootstrapとかdeviseとか使って横着したので、正直やった感ないなーって思いました。HTML/CSS多少勉強しなおして、あとこの本読んで、もう一回横着しないでなんちゃってQiitaみたいななんちゃってメディアシステムみたいなの作ろうかなと。そうなると、当然ユーザ情報の設計とか考えなきゃなんですが、いろいろ考えたら奥が深かったのでQiitaに載せて整理しようかなーって思いました。あと、ここに公開すればありがたいお言葉を頂戴いただけるのではないかとか思ったり。

本記事のユーザ情報の定義

利用用途

  • ログイン認証に用いる。
  • サインアップ画面より、新規登録をユーザ自身で可能とする。
  • ユーザによるプロフィール編集を可能とする。
  • プロフィールは他ユーザにより、CMS上のある画面から一覧表示できる。また、個別のユーザはユーザ名で検索可能とする。
  • プロフィール詳細はほかユーザにより、CMS上のある画面から確認可能とする。
  • ユーザ削除時は、ログイン情報も含めてすべて物理削除する

項目定義

  • 一意のログインID、パスワードをもつ。また、最終ログイン日時を保持する。
  • CMS内で利用される一意のユーザ名を保持する。
  • 上記ユーザ名とは別でユーザの氏名、氏名カナを保持する。ユーザの氏名、氏名カナは他ユーザは見れないものとする。
  • ユーザ登録時や運営お知らせに利用するためのメールアドレスを保持する。
  • プロフィール情報として年齢、誕生日、自由記述欄を保持する。

私の設計

こうなりました。詳細は追ってですが、この後簡単に思考プロセスを書いていきます。なお、簡単にするため、インデックスどうするとかは一旦考えません。
image.png

テーブルにどう持たせるか

まあ単純に考えるとusersテーブルにえいやって全項目突っ込みたくなりますよね。こんな感じです。
image.png
別に個人で勉強用途に使う程度ならこれでいいんですが、ある程度ユーザ増えるという前提で考えると、ユーザに関する全操作のたびにテーブルがロックされて、パフォーマンス落ちるんじゃない?とかデータ量多くなって適切にインデックス設計とかしても限界ありそうだよねとか思ったわけです。あと、このテーブルにカラム追加するとか、検索のためのタグ付けするとか、そういうの考えるとなんだか背筋が凍ります。特に住所情報持たせるとかなったらとんでもないことになりそう。

テーブルを分けてみる

要件に立ち返ると、認証、プロフィールとあるので、この観点で分けてみます。
image.png
ログインIDとパスワード、最終ログイン日時をauthenticatesとして定義しました。なお両テーブルは1:1で関連付けられています。
とりあえずusersテーブルのごった煮感はなくなりましたが、profileもう少しなんとかできないかなーとか思うので、もう少し分けてみます。
よく見るとほかユーザに見られてほしくない氏名の情報が混じってるのでこれは分けたほうがよさげ。と思ったのですが、、、
image.png
いい感じに分けられたんですけど、なんかセンスないというか、どう関連付けていいのかぱっと見わからないですね。。。
というわけで、fullnamesを親にして、profilesとauthenticatesと関連付けてみます。
image.png
なんかいい感じに見えました。でも親にfullnameってのがなんかあれですね。もう分けてしまいましょう。
image.png
いい感じになったけど、なんかこれでいいのか感があります。ちょっと大げさに分けすぎてしまったかな。。。
なんかもやっとしてますけど、これで終わりにします。名前のセンスないなとかそういうのはありますが。。。

最後に

もやっとした感じで終わってしまったので、なんか不完全燃焼ですが、これはこれで結論ということで。
authenticatesとusersの関連付けっているのかな。正直model側でdestroyの依存をかけてしまうだけでいい気もする。でも、直接SQLで触られたりすると一気にデータがおかしくなる。。。
あと、Railsの規約って観点でもどうなのかなとも少し思ったりもした。

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

ユーザ情報のDB設計について突き詰めて考えてみる

この記事について

  • システム開発するうえで絶対に避けては通れないユーザ情報周りの設計を自分なりに考察してみた
  • 結局何が最適かわからないけど色々と考え出すと奥が深かった

きっかけ的な

Railsでアプリ作ったんだけど、bootstrapとかdeviseとか使って横着したので、正直やった感ないなーって思いました。HTML/CSS多少勉強しなおして、あとこの本読んで、もう一回横着しないでなんちゃってQiitaみたいななんちゃってメディアシステムみたいなの作ろうかなと。そうなると、当然ユーザ情報の設計とか考えなきゃなんですが、いろいろ考えたら奥が深かったのでQiitaに載せて整理しようかなーって思いました。あと、ここに公開すればありがたいお言葉を頂戴いただけるのではないかとか思ったり。

本記事のユーザ情報の定義

利用用途

  • ログイン認証に用いる。
  • サインアップ画面より、新規登録をユーザ自身で可能とする。
  • ユーザによるプロフィール編集を可能とする。
  • プロフィールは他ユーザにより、CMS上のある画面から一覧表示できる。また、個別のユーザはユーザ名で検索可能とする。
  • プロフィール詳細はほかユーザにより、CMS上のある画面から確認可能とする。
  • ユーザ削除時は、ログイン情報も含めてすべて物理削除する

項目定義

  • 一意のログインID、パスワードをもつ。また、最終ログイン日時を保持する。
  • CMS内で利用される一意のユーザ名を保持する。
  • 上記ユーザ名とは別でユーザの氏名、氏名カナを保持する。ユーザの氏名、氏名カナは他ユーザは見れないものとする。
  • ユーザ登録時や運営お知らせに利用するためのメールアドレスを保持する。
  • プロフィール情報として年齢、誕生日、自由記述欄を保持する。

私の設計

こうなりました。詳細は追ってですが、この後簡単に思考プロセスを書いていきます。なお、簡単にするため、インデックスどうするとかは一旦考えません。
image.png

テーブルにどう持たせるか

まあ単純に考えるとusersテーブルにえいやって全項目突っ込みたくなりますよね。こんな感じです。
image.png
別に個人で勉強用途に使う程度ならこれでいいんですが、ある程度ユーザ増えるという前提で考えると、ユーザに関する全操作のたびにテーブルがロックされて、パフォーマンス落ちるんじゃない?とかデータ量多くなって適切にインデックス設計とかしても限界ありそうだよねとか思ったわけです。あと、このテーブルにカラム追加するとか、検索のためのタグ付けするとか、そういうの考えるとなんだか背筋が凍ります。特に住所情報持たせるとかなったらとんでもないことになりそう。

テーブルを分けてみる

要件に立ち返ると、認証、プロフィールとあるので、この観点で分けてみます。
image.png
ログインIDとパスワード、最終ログイン日時をauthenticatesとして定義しました。なお両テーブルは1:1で関連付けられています。
とりあえずusersテーブルのごった煮感はなくなりましたが、profileもう少しなんとかできないかなーとか思うので、もう少し分けてみます。
よく見るとほかユーザに見られてほしくない氏名の情報が混じってるのでこれは分けたほうがよさげ。と思ったのですが、、、
image.png
いい感じに分けられたんですけど、なんかセンスないというか、どう関連付けていいのかぱっと見わからないですね。。。
というわけで、fullnamesを親にして、profilesとauthenticatesと関連付けてみます。
image.png
なんかいい感じに見えました。でも親にfullnameってのがなんかあれですね。もう分けてしまいましょう。
image.png
いい感じになったけど、なんかこれでいいのか感があります。ちょっと大げさに分けすぎてしまったかな。。。
なんかもやっとしてますけど、これで終わりにします。名前のセンスないなとかそういうのはありますが。。。

最後に

もやっとした感じで終わってしまったので、なんか不完全燃焼ですが、これはこれで結論ということで。
authenticatesとusersの関連付けっているのかな。正直model側でdestroyの依存をかけてしまうだけでいい気もする。でも、直接SQLで触られたりすると一気にデータがおかしくなる。。。
あと、Railsの規約って観点でもどうなのかなとも少し思ったりもした。

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

LeetCode - 326. Power of Three

問題

326. Power of Three - 与えられた整数が3の累乗数かどうかを判定せよ

解答

ループまわすのと、再帰と考えてみる

ループ

# @param {Integer} n
# @return {Boolean}
def is_power_of_three(n)
  if n == 1
    true
  elsif n < 3
    false
  else
    while n != 3
      if n % 3 == 0
        n = n / 3
      else
        return false
        exit
      end
    end
    true
  end
end
  • 3 の累乗数ならば、商が 3 になるまで 3 で割りづつけても、あまりは 0 であるはず
  • X の 0 乗は 1 なので true

スコア

Runtime: 64 ms, faster than 85.71% of Ruby online submissions for Power of Three.
Memory Usage: 9.2 MB, less than 100.00% of Ruby online submissions for Power of Three.

再帰

# @param {Integer} n
# @return {Boolean}
def is_power_of_three(n, power_value = 1)
  #puts "n=#{n} power_value=#{power_value}"
  return true if n == power_value
  return false if n < power_value
  is_power_of_three(n, power_value * 3)
end
  • これは、LeetCodeのサイトにあった ぱっと思いつかなかったが、すばらしい解ですね
  • 3 の累乗数を小さいものから作っていき、どこかで nと 一致すれば n は 3 の累乗数、通り越してしまったら累乗数ではない

スコア

Runtime: 92 ms, faster than 20.00% of Ruby online submissions for Power of Three.
Memory Usage: 9.3 MB, less than 100.00% of Ruby online submissions for Power of Three.

きれいだけど速くはないんですね

テスト

require 'minitest/autorun'
require '../326-power-of-three.rb'

class DoTest < Minitest::Test

  def test_1
    assert_equal true, is_power_of_three(27)
  end

  def test_2
    assert_equal false, is_power_of_three(0)
  end

  def test_3
    assert_equal true, is_power_of_three(9)
  end

  def test_4
    assert_equal false, is_power_of_three(45)
  end

  def test_5
    assert_equal true, is_power_of_three(1)
  end

  def test_6
    assert_equal false, is_power_of_three(19684)
  end

  def test_7
    assert_equal false, is_power_of_three(-3)
  end

  def test_8
    assert_equal false, is_power_of_three(6)
  end

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

Railsチュートリアル bitbucketにプッシュできない

bitbucketにプッシュする方法。
軽くハマったので備忘録。

git push -u origin --all

だと失敗するので、

git push --mirror git@bitbucket.org:[ユーザー名]/sample_app

こっちを使いましょう。

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

[Ruby on Rails]画像投稿ボタンをFont Awesomeのアイコンにする方法

概要

表題の通りですが、調べるとHaml記法で書かれた投稿しか見つけられず、今のところHaml記法を使っていない私にはわかりづらかったので、メモがてら共有させていただきます。

環境

Ruby:2.6.3
Rails:5.1.6
bootstrap:4.4.1
FontAwesome

方法

1.file_fieldで画像投稿ボタンを表示。
2.file_fieldボタンによって出力されるinputタグを見えなくする(dislay:none)。
3.file_fieldの上にFont Awesomeのアイコンをiタグで表示。
4.iタグをlabelタグで囲って、アイコンをボタンとして有効にする。

home.html.erb
<%= form_for(@dreampost) do |f| %>
  <div class="field">
    <%= f.text_area :content, placeholder: "投稿できます" %>
  </div>
  <div class="space-between">
    <span class="picture">
     <label for="dreampost_picture">
       <i class="far fa-image"></i>
      </label>    
      <%= f.file_field :picture, placeholder: '&#xf0a8',accept: 'image/jpeg,image/gif,image/png' %>
      <div class="clear"></div> 
    </span>
  <%= f.submit "送信", class: "btn btn-primary" %>
<% end %>
custom.scss
.space-between { display: flex; justify-content: space-between; }

.picture>input { display: none; }

.picture>label { margin-bottom: 0; float: left; }

.fa-image { color: #fff; float: left; }

.fa-image::before { font-size: 2rem; }

.clear { clear: both; }

スクリーンショット 2020-02-22 10.08.56.png

補足

div.classに{ display: flex; justify-content: space-between;}と、
labelタグとiタグにfloat:leftで、
アイコンを左寄せ、送信ボタンを右寄せにしています。

ご指摘などございましたら、ぜひよろしくお願いいたします。

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

Rails でトークン認証 API を 15 分で実装する

下記を作成してみます。SPA のバックエンド側を想定しています。

  • /auth でログインし token を発行してもらえる。
  • /users でユーザー一覧を取得できる。
    • ただし token が必要。
    • 更に admin ユーザーのみでき、member ユーザーはアクセス許可がない。

上記を利用したフロント側の記事も書いておりますので宜しければご覧ください。
Vue.js で簡単なログイン画面 (トークン認証) を作ってみた

User API 作成

rails new で API モードで新規アプリ作成します。

$ rails new yourappname --api

scaffold で User Model と Controller を作ります。
lock_version というカラムを追加すると Rails で楽観ロックを実装してくれます。便利ですね。

$ cd yourappname
$ rails g scaffold User name:string \
email:string \
role:integer \
password_digest:string \
register_user:integer \
update_user:integer \
lock_version:integer \
activated_at:datetime \
deleted_at:datetime

seeds に初期データを書いてみましょう。

db/seeds.rb
User.create!([
  {
    name: 'admin',
    email: 'admin@example.com',
    role: 'admin',
    password_digest: '$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK',
    register_user: 1,
    update_user: 1,
    lock_version: 0,
    activated_at: '2020-02-03 00:00:00',
    deleted_at: nil,
  },
  {
    name: 'member',
    email: 'member@example.com',
    role: 'member',
    password_digest: '$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK',
    register_user: 1,
    update_user: 1,
    lock_version: 0,
    activated_at: '2020-02-03 00:00:00',
    deleted_at: nil,
  },
])

開発用の DB をマイグレーション & 初期データを投入します。

$ rails db:migrate && rails db:seed

サーバーを起動します。

$ rails s

別ターミナルにて curl で叩くと初期データが返ってきます。
まずは簡単な API ができたことを確認します。:smile:

# 別ターミナル
$ curl -s http://localhost:3000/users | jq
[
  {
    "id": 1,
    "name": "admin",
    "email": "admin@example.com",
    "role": "admin",
    "password_digest": "$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK",
    "register_user": 1,
    "update_user": 1,
    "lock_version": 0,
    "activated_at": "2020-02-03T00:00:00.000Z",
    "deleted_at": null,
    "created_at": "2020-02-22T04:08:58.769Z",
    "updated_at": "2020-02-22T04:08:58.769Z"
  },
  {
    "id": 2,
    "name": "member",
    "email": "member@example.com",
    "role": "member",
    "password_digest": "$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK",
    "register_user": 1,
    "update_user": 1,
    "lock_version": 0,
    "activated_at": "2020-02-03T00:00:00.000Z",
    "deleted_at": null,
    "created_at": "2020-02-22T04:08:58.775Z",
    "updated_at": "2020-02-22T04:08:58.775Z"
  }
]

テストも実行してエラーが無いことも確認しましょう。

$ rails t
Running via Spring preloader in process 16858
Run options: --seed 26013

# Running:

.....

Finished in 0.320389s, 15.6060 runs/s, 21.8484 assertions/s.
5 runs, 7 assertions, 0 failures, 0 errors, 0 skips

ちなみに jq が入っていない場合は入れると JSON 整形してくれて便利です。

$ brew install jq

Gemfile もろもろ

Gemfile は Node.js で言う、package.json 的なファイルだと思います。

下記を追記します。ついでにオススメの gem も。

gem 'redis-rails' # Redis を扱うための gem
gem 'mock_redis' # Redis のモック。テスト実行時に使用。
gem 'config' # 環境ごとに yml の設定ファイルを作成可能。
gem 'pundit' # 認証周りを REST ベースでシンプルに実装できる。
gem 'paranoia' # 論理削除できる。

下記をコメントアウトします。
has_secure_password を使う際に必要です。
これはテーブルに password_digest というカラムを用意すると、Rails がパスワードをハッシュ化してくれます。

gem 'bcrypt', '~> 3.1.7'

bundle install するとパッケージがインストールされます。

$ bundle install

config をインストールすると rails g config:install が使えます。
環境ごとに yml ファイルができます。便利ですね。

$ rails g config:install
Running via Spring preloader in process 17408
      create  config/initializers/config.rb
      create  config/settings.yml
      create  config/settings.local.yml
      create  config/settings
      create  config/settings/development.yml
      create  config/settings/production.yml
      create  config/settings/test.yml
      append  .gitignore

Redis 設定

セッション情報を保存するためにインメモリ DB の Redis を使います。
Mac の方は brew でインストール。

$ brew install redis

redis-server で起動できます。簡単でいいですね。

$ redis-server

いったん、全環境共通の settings.yml に url と timeout を追記します。
こうすると例えば、下記の url の値を取り出すには Settings.session.url と記述すれば OK です。

config/settings.yml
session:
  url: redis://localhost:6379
  timeout: 7200
role:
  member: member
  admin: admin

ところで Redis 接続時に毎回 Redis.new() を書くのは不便なので、initializers/ 下に redis.rb を作成します。
ついでにテストの時は Redis 立ち上げなくても済むようにしました。

config/initializers/redis.rb
if Rails.env.test?
  REDIS = MockRedis.new
else
  REDIS = Redis.new(url: Settings.session.url)
end

Auth コントローラー作成

コマンドラインから枠を作ります。

$ rails g controller auth

auth のルーティングを追加します。
個人的にはなるべく resources を使うようにすると綺麗だと思います。

config/routes.rb
Rails.application.routes.draw do
  resources :users
+  resources :auth, :only => [:create, :destroy]
end

セッション作成処理を concerns に切り出してみます。

app/controllers/concerns/session.rb
module Session
  def self.create(user)
    token = SecureRandom.hex(64)
    REDIS.mapped_hmset(
      token,
      'user_id' => user.id,
      'role' => user.role,
    )
    REDIS.expire(token, Settings.session.timeout)
    return token
  end
end

AuthController で先ほど作った Session モジュールを利用します。

app/controllers/auth_controller.rb
class AuthController < ApplicationController
+  def create
+    user = User.find_by(email: params[:email])
+    token = ''
+    status = :unauthorized
+    if user && user.authenticate(params[:password])
+      token = Session.create(user)
+      status = :created
+    end
+    render json: { token: token }, status: status
+  end
end

models/user.rb に下記を追記。
物理削除とパスワードハッシュ化を利用します。
enum で member と admin も定義しておきます。

app/models/user.rb
class User < ApplicationRecord
+  acts_as_paranoid
+  has_secure_password
+  enum role: { member: 0, admin: 1 }
end

動作確認してみましょう。

ログイン OK

まずは seeds.rb に記述した email と password で /auth を叩きます。
token が返ってきますね:smile:

$ curl -s \
-X POST http://localhost:3000/auth \
-H "Content-Type: application/json" \
-d '{"email": "admin@example.com", "password": "password1234"}' 

{"token":"ed64bbcf830e4feca9203b0955c5916fda815a23da80da8a7bb62b8c6466dbc34a07e21e9dd96447882fc8b9cb06121c8ee414b79ffedef4892e7197bbe9edd1"}

redis-cli で Redis に接続して、トークンが作成されたか確認してみましょう。
おお、できています!:joy:

$ redis-cli
127.0.0.1:6379> keys *
1) "ed64bbcf830e4feca9203b0955c5916fda815a23da80da8a7bb62b8c6466dbc34a07e21e9dd96447882fc8b9cb06121c8ee414b79ffedef4892e7197bbe9edd1"

ログイン NG

password を適当に変えてみます。
token が空になっていますね。

$ curl -s -X POST http://localhost:3000/auth \
-H "Content-Type: application/json" \
-d '{"email": "admin@example.com", "password": "wrongpassword"}'

{"token":""}

認証を設定

Session モジュールにセッション情報を取得する function を追加します。

app/controllers/concerns/session.rb
module Session

+  def self.get(token)
+    REDIS.hgetall(token)
+  end

  def self.create(user)
    token = SecureRandom.hex(64)
    REDIS.mapped_hmset(
      token,
      'user_id' => user.id,
      'role' => user.role,
    )
    REDIS.expire(token, Settings.session.timeout)
    return token
  end
end

続いて基幹コントローラーに手を入れます。

  • authenticate_with_http_token を利用すると、 リクエストヘッダに 'Authorization: Token hogehoge' がセットされていた場合に、トークン hogehoge を取り出せます。
    • 上記を利用するには include ActionController::HttpAuthentication::Token::ControllerMethods する必要があります。
  • before_action :set_session で、Redis に登録してあるセッション (ここでは user.id と user.role ) をメンバにセットしています。
  • before_action :require_login で、基本的にセッションがない場合に認証エラーとしています。
app/controllers/application_controller.rb
class ApplicationController < ActionController::API
+  include ActionController::HttpAuthentication::Token::ControllerMethods
+
+  before_action :set_session
+  before_action :require_login
+
+  @session = {}
+
+  def require_login
+    render json: { error: 'unauthorized' }, status: :unauthorized if @session.empty?
+  end
+
+  private
+    def set_session
+      authenticate_with_http_token do |token, options|
+        @session = Session.get(token)
+      end
+    end
end

AuthController に skip_before_action を追加します。
これを追加しないと一生ログインができません・・。

app/controllers/auth_controller.rb
class AuthController < ApplicationController
+  skip_before_action :require_login, only: [:create]
+
  def create
    user = User.find_by(email: params[:email])
    token = ''
    status = :unauthorized
    if user && user.authenticate(params[:password])
      token = Session.create(user)
      status = :created
    end
    render json: { token: token }, status: status
  end
end

ユーザー一覧 OK

Authorization: Token xxx には、先ほど /auth を叩いて得られた token をセットします。
/users の結果が返ってきています。

$ curl -s http://localhost:3000/users \
-H "Content-Type: application/json" \
-H "Authorization: Token ed64bbcf830e4feca9203b0955c5916fda815a23da80da8a7bb62b8c6466dbc34a07e21e9dd96447882fc8b9cb06121c8ee414b79ffedef4892e7197bbe9edd1" \
| jq

[
  {
    "id": 1,
    "name": "admin",
    "email": "admin@example.com",
    "role": 0,
    "password_digest": "$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK",
    "register_user": 1,
    "update_user": 1,
    "lock_version": 0,
    "activated_at": "2020-02-03T00:00:00.000Z",
    "deleted_at": null,
    "created_at": "2020-02-21T14:28:10.627Z",
    "updated_at": "2020-02-21T14:28:10.627Z"
  },
  {
    "id": 2,
    "name": "member",
    "email": "member@example.com",
    "role": 0,
    "password_digest": "$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK",
    "register_user": 1,
    "update_user": 1,
    "lock_version": 0,
    "activated_at": "2020-02-03T00:00:00.000Z",
    "deleted_at": null,
    "created_at": "2020-02-21T14:28:10.635Z",
    "updated_at": "2020-02-21T14:28:10.635Z"
  }
]

ユーザー一覧 NG

token を適当なものに変えるとちゃんと認証エラーになりました:relaxed:

$ curl -s http://localhost:3000/users -H "Content-Type: application/json" -H "Authorization: Token wrong_token" | jq

{
  "error": "unauthorized"
}

権限周り設定

Web アプリでは権限は必須と言えます。
Pundit を使うと Rest ベースでシンプルに実装できます。

$ rails g pundit:install
$ rails g pundit:policy user

まずは application_policy.rb に管理者かどうかの admin? を追加してみます。

app/policies/application_policy.rb
class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def index?
    false
  end

  def show?
    false
  end

  def create?
    false
  end

  def new?
    create?
  end

  def update?
    false
  end

  def edit?
    update?
  end

  def destroy?
    false
  end

+  def admin?
+    @user['role'] == Settings.role.admin
+  end

  class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user = user
      @scope = scope
    end

    def resolve
      scope.all
    end
  end
end

続いて、user_policy.rb の index に先ほど作った admin? を追加します。

app/policies/user_policy.rb
class UserPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      scope.all
    end
  end

+  def index?
+    admin?
+  end
end

あとは利用側です。
application_controller.rb で Pundit を include し、current_user メソッドを追加します。
また、Pundit の NotAuthorizedError を拾えるように rescue_from を追加します。
(エラー処理は増えてきたら concerns に切り出すと良いと思います。)

app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include ActionController::HttpAuthentication::Token::ControllerMethods
+  include Pundit

  before_action :set_session
  before_action :require_login

  @session = {}

+  rescue_from Pundit::NotAuthorizedError do |e|
+    render json: { detail: e.message }, status: :unauthorized
+  end
+
  def require_login
    render json: { error: 'unauthorized' }, status: :unauthorized if @session.empty?
  end

+  def current_user
+    @session
+  end

  private
    def set_session
      authenticate_with_http_token do |token, options|
        @session = Session.get(token)
      end
    end
end

後は users_controller.rb の index に Pundit を追加します。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :set_user, only: [:show, :update, :destroy]

  # GET /users
  def index
-    @users = User.all
-
-    render json: @users
+    users = authorize Pundit.policy_scope(@session, User)
    +    render json: users
  end

  # GET /users/1
  def show
    render json: @user
  end

  # POST /users
  def create
    @user = User.new(user_params)

    if @user.save
      render json: @user, status: :created, location: @user
    else
      render json: @user.errors, status: :unprocessable_entity
    end
  end

  # PATCH/PUT /users/1
  def update
    if @user.update(user_params)
      render json: @user
    else
      render json: @user.errors, status: :unprocessable_entity
    end
  end

  # DELETE /users/1
  def destroy
    @user.destroy
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_user
      @user = User.find(params[:id])
    end

    # Only allow a trusted parameter "white list" through.
    def user_params
      params.require(:user).permit(:name, :email, :role, :password_digest, :register_user, :update_user, :lock_version, :activated_at, :deleted_at)
    end
end

さて、動作確認してみましょう。
まずは member ユーザーでログインし、/users を叩きます。
ユーザー一覧が取得できないことを確認します。

$ curl -s -X POST http://localhost:3000/auth -H "Content-Type: application/json" -d '{"email": "member@example.com", "password": "password1234"}'
{"token":"482125111d11c2882cad25b700221a45bc64d8745771f427cb8039337c0e7cda95fc8498f98d6d784781b177afe84699bccb2d62628b1bee3ccee0e96eb9e576"}

$ curl -s http://localhost:3000/users -H "Content-Type: application/json" -H "Authorization: Token 482125111d11c2882cad25b700221a45bc64d8745771f427cb8039337c0e7cda95fc8498f98d6d784781b177afe84699bccb2d62628b1bee3ccee0e96eb9e576"
{"error":"not allowed to index? this User::ActiveRecord_Relation"}

続いて admin ユーザーでログインし、/users を叩きます。
結果が取得できましたね:smile:

$ curl -s -X POST http://localhost:3000/auth -H "Content-Type: application/json" -d '{"email": "admin@example.com", "password": "password1234"}'
{"token":"a87dee3d5cb1592e3b3b09f78931e26254292f1453ea77a1d491fd949ce508d5ea7e00dec6163fb83ed9c7f5d39b672aa429c770a6d0487952022fc55a493487"}

$ curl -s http://localhost:3000/users -H "Content-Type: application/json" -H "Authorization: Token a87dee3d5cb1592e3b3b09f78931e26254292f1453ea77a1d491fd949ce508d5ea7e00dec6163fb83ed9c7f5d39b672aa429c770a6d0487952022fc55a493487"
[{"id":1,"name":"admin","email":"admin@example.com","role":"admin","password_digest":"$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK","register_user":1,"update_user":1,"lock_version":0,"activated_at":"2020-02-03T00:00:00.000Z","deleted_at":null,"created_at":"2020-02-11T12:14:52.557Z","updated_at":"2020-02-11T12:14:52.557Z"},{"id":2,"name":"member","email":"member@example.com","role":"member","password_digest":"$2a$10$HjQH2VBdguACJLyZHoVSs.yBZbwypqY3vUJGnxlWj94rmilWIuWzK","register_user":1,"update_user":1,"lock_version":0,"activated_at":"2020-02-03T00:00:00.000Z","deleted_at":null,"created_at":"2020-02-11T12:14:52.565Z","updated_at":"2020-02-11T12:14:52.565Z"}]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

正規表現_validations_定められた文字を文字列に含む

【問題】
2行目のvalidationを完成させてください。ただし条件は、「TECH::EXPERT」という文字が投稿された文字列に含まれていることを確かめること。

class Impression < ActiveRecord::Base
VALID_YOUTUBE_URL_REGEX = ???
validates :url, presence: true, format: { with: VALID_TECH::EXPERT_URL_REGEX }
end

【解答】
class Impression < ActiveRecord::Base
VALID_YOUTUBE_URL_REGEX = /\A.TECH::EXPERT.\z/
validates :text, presence: true, format: { with: VALID_YOUTUBE_URL_REGEX }
end

【解説】
class Impression < ActiveRecord::Base

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

正規表現validation_定められた文字を文字列に含む

【問題】
2行目のvalidationを完成させてください。ただし条件は、「YOUTUBE」という文字が投稿された文字列に含まれていることを確かめること。

class Impression < ActiveRecord::Base
VALID_YOUTUBE_URL_REGEX = ???
validates :url, presence: true, format: { with: VALID_YOUTUBE_URL_REGEX }
end

【解答】
class Impression < ActiveRecord::Base
VALID_YOUTUBE_URL_REGEX = /\A.* YOUTUBE. *\z/
validates :text, presence: true, format: { with: VALID_YOUTUBE_URL_REGEX }
end

【解説】
1)class Impression < ActiveRecord::Base

RailsでActive Recordのバリデーション (検証: validation) 機能を使って、オブジェクトがデータベースに保存される前にオブジェクトの状態を検証。バリデーションは、正しいデータだけをデータベースに保存するために行われます。正しいデータだけをデータベースに保存するのであれば、model.rbモデルレベルでバリデーションを実行するのが最適です。モデルレベルでのバリデーションは、データベースに依存せず、エンドユーザーがバイパスすることもできず、テストもメンテナンスもやりやすいためです。Railsではバリデーションを簡単に利用できるよう、一般に利用可能なビルトインヘルパーが用意されており、独自のバリデーションメソッドも作成できるようになっています。

2)VALID_YOUTUBE_URL_REGEX = /\A.* YOUTUBE .*\z/

Regexは、「Regular Expression、正規表現」の略。

「/」正規表現オブジェクトを作成する時に「/パターン/」の形式で作成が可能となるため操作したい文字列をスラッシュで挟む
「\A」文字列の先頭
「\z」文字列の末尾
「.」は改行を除く任意の1文字にマッチ。ただし、複数行モードでは改行にもマッチする(例は後述の参考URLの3番目ご参照)
「*」は直前の正規表現の0回以上の反復(例は後述の参考URL、最後のURLご参照)

3)validates :text, presence: true, format: { with: VALID_YOUTUBE_URL_REGEX }

validates: 検証対象のフィールド名を指定します。, presence: true presenceヘルバーは、指定された属性が「空でない」ことを確認します。trueで常に検証することを指定する、つまり入力済みかを常に検証, format: with:属性の値}
formatヘルパーは、withオプションで与えられた正規表現と属性の値がマッチするかどうかのテストによる検証を行います。デフォルトのエラーメッセージは「is invalid」です。

【参考】
https://railsguides.jp/active_record_validations.html
https://docs.ruby-lang.org/ja/latest/doc/spec=2fregexp.html
https://www.javadrive.jp/ruby/regex/repeat/index1.html
https://www.javadrive.jp/ruby/regex/repeat/index2.html

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

正規表現validation_定められた文字が投稿された文字列に含まれる

【問題】
2行目のvalidationを完成させてください。ただし条件は、「YOUTUBE」という文字が投稿された文字列に含まれていることを確かめること。

class Impression < ActiveRecord::Base
VALID_YOUTUBE_URL_REGEX = ???
validates :url, presence: true, format: { with: VALID_YOUTUBE_URL_REGEX }
end

【解答】
class Impression < ActiveRecord::Base
VALID_YOUTUBE_URL_REGEX = /\A.* YOUTUBE. *\z/
validates :text, presence: true, format: { with: VALID_YOUTUBE_URL_REGEX }
end

【解説】
1)class Impression < ActiveRecord::Base

RailsでActive Recordのバリデーション (検証: validation) 機能を使って、オブジェクトがデータベースに保存される前にオブジェクトの状態を検証。バリデーションは、正しいデータだけをデータベースに保存するために行われます。正しいデータだけをデータベースに保存するのであれば、model.rbモデルレベルでバリデーションを実行するのが最適です。モデルレベルでのバリデーションは、データベースに依存せず、エンドユーザーがバイパスすることもできず、テストもメンテナンスもやりやすいためです。Railsではバリデーションを簡単に利用できるよう、一般に利用可能なビルトインヘルパーが用意されており、独自のバリデーションメソッドも作成できるようになっています。

2)VALID_YOUTUBE_URL_REGEX = /\A.* YOUTUBE .*\z/

Regexは、「Regular Expression、正規表現」の略。

「/」正規表現オブジェクトを作成する時に「/パターン/」の形式で作成が可能となるため操作したい文字列をスラッシュで挟む
「\A」文字列の先頭
「\z」文字列の末尾
「.」は改行を除く任意の1文字にマッチ。ただし、複数行モードでは改行にもマッチする(例は後述の参考URLの3番目ご参照)
「*」は直前の正規表現の0回以上の反復(例は後述の参考URL、最後のURLご参照)

3)validates :text, presence: true, format: { with: VALID_YOUTUBE_URL_REGEX }

validates: 検証対象のフィールド名を指定します。, presence: true presenceヘルバーは、指定された属性が「空でない」ことを確認します。trueで常に検証することを指定する、つまり入力済みかを常に検証, format: with:属性の値}
formatヘルパーは、withオプションで与えられた正規表現と属性の値がマッチするかどうかのテストによる検証を行います。デフォルトのエラーメッセージは「is invalid」です。

【参考】
https://railsguides.jp/active_record_validations.html
https://docs.ruby-lang.org/ja/latest/doc/spec=2fregexp.html
https://www.javadrive.jp/ruby/regex/repeat/index1.html
https://www.javadrive.jp/ruby/regex/repeat/index2.html

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

Ruby on Rails チュートリアル学習記録 第1章

 はじめに

初投稿なので簡単に自己紹介。
私は独学でプログラミング学習を始めたアラサーの男性です。
未経験からエンジニアへの転職を目指しています。
ProgateでHTML/CSS/Ruby/Ruby on Rails5をひと通り学習後Railsチュートリアル開始。
Progateと違い詰まったら自分で調べたり考えたりしながら進めていく必要がありそうなので学習記録を付けることにしました。
主に自分が詰まったところについて書いていくつもりなので章ごとに文章量は疎らになっていくと思います。

1.1

特になし

1.2

特になし

1.3

gemfileの内容を書き換えbundle installを実行するとエラー発生

You have requested:
  spring = 2.0.2

The bundle currently has spring locked at 2.1.0.
Try running `bundle update spring`

If you are updating multiple gems in your Gemfile at once,
try passing them all to `bundle update`

bundle updateを実行後、bundle installを再度実行で解決。

1.4

Rails チュートリアルにおいては、マニュアル作成時点ではGithubで非公開型レポジトリを無料で使えなかったためにBitbucketを使用していようだが、現在はGitHubでも非公開型レポジトリを無料で使えるようになっていたので、自分は既にアカウントを作っていたGitHubを使用。
GitHubでもチュートリアル通りに進めて特に問題は発生せず。

1.5

特になし

1.6

特になし

感想

マニュアル通りに進めていれば特に困ることはなかった。
正直やってることの意味はきちんと理解できていない部分もあると思うが、どうせ2週やるつもりなのでそこはスピード感重視で。
ファイルの内容を変更した後、保存をし忘れてうまくいかないというケアレスミスが何度かあったので気を付けたい。

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

【これで無理なら諦めろ】オブジェクト指向を世界一分かりやすく説明する

はじめに

個人的にオブジェクト指向を理解するのに苦戦しまくったので、その真髄を世界一分かりやすく説明していきます。
何か間違いなどあればどんどんご教示お願いします。言語はRubyです。

対象読者

  • Ruby、Railsを勉強中の方
  • 業務でRubyを使っている方
  • チェリー本やRailsチュートリアルで挫折しちゃった人

オブジェクト指向とは

オブジェクト指向とは、プログラムを「手順」ではなく、「モノの作成と反応」として見る考え方です。これだけではイマイチ分からないと思うので、具体的なコードを使って説明していきます。

オベジェクト指向を理解するには、手続き型のコードを知るのが早いです。

例えば、ある数字を加工して、その数字を使って文章を作って、結果を出力するとき、手続き型では以下のように書きます。

def calculate(number_1, number_2)
  number_1 * number_2 + 1
end

def update_content(value)
  "結果は#{value}です"
end

def output(content)
  puts number
end

value = 100
content = ""

new_value = calculate(value,20)
// 2001

content = update_content(new_value)
// "結果は2001です"

output(content)
=> "結果は2001です"

一方で、オブジェクト指向で書くとこんな感じ。

num = Num.new(100,"")
num.calculate(20)
// 2001

num.update_content
// "結果は2001です"

num.output
=> "結果は2001です"

なんとなく違いは分かりますか?
前者は、先に関数を定義して、処理を実行するたびにいろんなメソッドに値を飛ばして、返ってきた値を変数に代入して...
と流れるようにプログラムが進んでいきます。

なんだか忙しそうですよね?

一方で後者は、はじめにnumというモノを作成し、そいつに対して「計算して」「中身を更新して」「出力して」と呼びかけるような感じで処理が進んでいきます。

情報の記憶も処理の内容も「モノ」が全部やってくれるので、一度作ってしまえば見通しよく実行できるわけです。

この後者こそがまさに、オブジェクト指向の考え方であり、今回でいう所のnumオブジェクトと呼ばれるものです。

つまり、オブジェクト指向とは、コードにオブジェクトという主人公を登場させ、それをメインに話を進めていく設計の考え方といえます。

やっぱり、ドラマを見るにも、主人公がいた方が面白いし分かりやすいじゃないですか。
プログラミングも同じで、処理の中に主人公がいた方が読みやすくて扱いやすくなるわけです。

ちなみにnumというオブジェクトは以下のクラスから作成されていました。へぇ〜ぐらいで流してください。

num.rb
class Num
  attr_accessor :value, :content

  def initialize(value, content)
    @value = value
    @content = content
  end

  def calculate(number)
    self.value = value * number + 1
  end

  def update_content
    self.content = "計算結果は#{value}です"
  end

  def output
    puts content
  end
end

num = Num.new(100,"")
num.calculate(100)
num.update_content
num.output

オブジェクトとは

先ほどの例で少しはオブジェクト指向の雰囲気を掴めたかと思います。
でば、オブジェクトとは一体なんでしょうか?

こいつの正体を掴むのに僕は半年ほどかかりました。そして、僕が腑に落ちた例えで分かりやすーーーく説明します。

結論から言うと、オブジェクトとは魔法の箱です。

image.png

この魔法の箱は2つの性質を持ちます。

  1. 情報を持つ
  2. 反応する

プログラミングをやっていると、オブジェクトが色んなことをしているように見えますが、結局やっていることはこの2つだけなんです。

情報を保持するから「箱」、メッセージに対して反応できるから「魔法の」と言う例えにしました。

オブジェクトとは、情報を持ち、メッセージに対して反応するただの箱なんだ。

とりあえず今の段階ではそのように理解してもらえれば大丈夫です。

1. 情報を持つとは?

オブジェクトは情報を持つことができます。分かりやすいのが以下の例。

user.name 
=> 山田
user.age 
=> 18

これはuserという魔法の箱がnameageという2つの情報を持っていることを示しています。すごく簡単ですね。

この情報はいつでも呼び出すことができますし、その気になれば更新することも消すこともできます。

2. 反応するとは?

オブジェクトはあるメッセージに対して反応することもできます。

user.hello
=> "おはようございます。"

これまで5万回は見たhelloの例です。
これは、helloというメッセージを伝えたら"おはようございます。"と返すようにuserという魔法の箱が反応したと見ることができます。

userをレシーバ、helloをメッセージと呼ぶのはこのような理由です。

ちなみに、そこらへんのゴミ箱にhelloと話しかけても何も返事しません。

dustbox.hello
=> NameError (undefined local variable or method `dustbox' for main:Object)

userという箱が反応してくれたのは、事前に反応パターンを記憶させたからです。

class User
  def hello
    puts "おはようございます。"
  end
end

このように、魔法の箱(オブジェクト)に教え込む反応方法のことをメソッドと言います。

では、この教えこむ場所のことを(学校の)クラスと呼ベばかなり分かりやすいのではないかと閃きました。
言い換えれば、classとは魔法の箱の養成クラスとでもいいましょうか。

image.png

すごく簡単にまとめると...

  • オブジェクトとは魔法の箱である
  • 魔法の箱は、情報を持ったり、メッセージに対して反応したりできる
  • 魔法の箱に教え込む反応パターンのことをメソッドと呼ぶ
  • 反応パターンを教え込む場所のことをクラスと呼ぶ
  • オブジェクト指向とは、プログラムを「手順」ではなく、「魔法の箱(オブジェクト)の反応」と見る考え方

以上が、オブジェクト指向の説明になります。
この記事でオブジェクト指向への理解が少しでも深まれば幸いです。

追記1

Numクラスのソースコードに間違いがあったため、修正いたしました。
ご指定いただいた方々ありがとうございます。

変更点

  • Class Num => class Numへ修正
  • 属性名をnum => valueへ変更。(属性名とインスタンス名が同じだとややこしいため)
  • caluculateメソッドにおける、value = => self.value に修正(属性を上書きする際はself.の省略はできないようです。)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails で簡単!】PAY.JPを利用したクレジットカード決済の導入

Image from Gyazo

何かサービスを作る際に、決済機能を導入したいはずです。
今回はPAY.JPを利用した決済機能を案内していきます。

PAY.JPの導入準備

スクリプトの記述

PAY.JPを使うためのスクリプトを記述します。
下記をコピーしてください。

スクリプト
%script{src: "https://js.pay.jp/", type: "text/javascript"}

コピーしたら、application.html.hamlに貼り付けましょう!!

application.html.haml
%html
  %head
    %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
    %title payjptest
    %script{src: "https://js.pay.jp/", type: "text/javascript"}
    -# このscriptを記載
    = csrf_meta_tags
    = stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
  %body
    = yield

PAY.JPの登録

PAY.JPのアカウントを作成しましょう!
https://pay.jp/

APIキーを取得します

アカウントを作成してログインし、下記の場所のAPIキーを確認しましょう!

Image from Gyazo

Appにgem 'payjp'を追加

Gemfile
# PAY.JPのgem
gem 'payjp'

# 環境変数を簡単に定義できるENVファイルを対応させるgem
gem 'dotenv-rails'

追加したら

ターミナル
$ bundle install
$ rails s 

再起動しないとgemもscriptも読み込まれないので、エラーがおきます。

環境変数を利用して、APIキーをAppに登録

gem 'dotenv-rails'をインストールできたので、.envファイルを作成しましょう

app > .env の場所に作成します。

Image from Gyazo

.gitignoreの上だと思えば簡単です。

秘密鍵をGithubにコミットしてしまうとAPIキーを世に公開してしまうので、.gitignoreに.envを記述します

gitignore
/.env

ここ本当に重要なので、注意してくださいね!

では、APIキーを記述します

.env
PAYJP_PRIVATE_KEY     = 'sk_test_111111111111111111111111'
PAYJP_KEY             = 'pk_test_111111111111111111111111'

記載したら、Githubのコミットに表示されていないか?確認します。
コミットに表示されてたら、ヤバイです。
危険です。.gitignoreを再確認してください。

ここまでで準備完了です。

素材作成(view):ここは自身で記述すると良いかと思います。

ここはviewですが、必要なければ飛ばしてください。

クレジットカードを登録するためのviewを作成します。
下記はformの部分テンプレートですが下記のような

スクリプト

記述
#{asset_path 'creditcards/master-card.svg'}

Image from Gyazo

master-card.svgの画像素材がないとエラーがおきますので、
コピペするなら素材を集めてください。

form

formの部分テンプレート
= form_with url:creditcards_path, method: :post, html: { name: "inputForm" },class:"form" do |f|
  .form__upper
    .form__upper__group
      = f.label :カード番号
      %span.form-require 必須
      = f.text_field :card_number, name: "card_number", id:"card_number", type: "text", placeholder: '半角数字のみ', class: 'input-default', maxlength: "16"
      %ol
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/visa.svg'}", width:"35", height:"20"}
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/master-card.svg'}", width:"35", height:"20"}
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/saison-card.svg'}", width:"35", height:"20"}
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/jcb.svg'}", width:"35", height:"20"}
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/american_express.svg'}", width:"35", height:"20", class:"american_express"}
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/dinersclub.svg'}", width:"35", height:"20"}
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/discover.svg'}", width:"35", height:"20"}
    .form__upper__group.exp
      .name
        = f.label :有効期限
        %span.form-require 必須
      = f.select :exp_year, [["19",2019],["20",2020],["21",2021],["22",2022],["23",2023],["24",2024],["25",2025],["26",2026],["27",2027],["28",2028],["29",2029]], {}, class: 'input-default harf', name: "exp_year", id:"exp_year"
      %span= f.select :exp_month, [["01",1],["02",2],["03",3],["04",4],["05",5],["06",6],["07",7],["08",8],["09",9],["10",10],["11",11],["12",12]],{}, class: 'input-default harf', name: "exp_month", id:"exp_month"
      %span.form__upper__group
      = f.label :セキュリティーコード
      %span.form-require 必須
      = f.text_field :cvc, name: "cvc", id:"cvc", class:"cvc", type: "text", placeholder: 'カード背面4桁もしくは3桁の番号', class: 'input-default', maxlength: "16"
    .form__upper__group
      %p.about
        = fa_icon 'question-circle'
        %span
        = link_to 'カード裏面の番号とは?', root_path, class:'about__registered'
    .form__upper__group
      = f.submit '次へ進む', class: 'btn-default', id: "charge-form"

data: "#{asset_path 'creditcards/visa.svg'}"は、
【 app > asset > images > creditcards > visa.svg 】を読み込む
という設定になります。

scss
.form{
  &__upper{
    margin: 0 auto;
    max-width: 343px;
    p {
      text-align: center;
    }
    &__group {
      font-size: 14px;
      color: #333;
      &:not( :first-child ){
        margin-top: 32px;
      }
      label{
        font-weight: 600;
      }
      ol{
        display:flex;
        li {
          margin: 5px 8px 0 0;
        }
      }
      .form-require {
        background-color: $green;
        color: #fff;
        font-size: 12px;
        margin: 0 0 0 8px;
        padding: 2px 4px;
        border-radius: 2px;
        vertical-align:top;
        &-optional {
          background-color: gray;
          color: #fff;
          font-size: 12px;
          margin: 0 0 0 8px;
          padding: 2px 4px;
          border-radius: 2px;
          vertical-align:top;
        }
      }
      .input-default{
        width: 90%;
        margin: 8px 0 0;
        height: 48px;
        padding: 10px 16px 8px;
        border-radius: 4px;
        border: 1px solid #ccc;
        background: #fff;
        line-height: 1.5;
        font-size: 16px;
        &.harf{
          width: calc(40% - 6px);
          margin: 8px 8px 0 0;
          height: 45px;
          border-radius: 4px;
          border: 1px solid #ccc;
          background: #fff;
          line-height: 1.5;
          font-size: 16px;
          &.exp{
            -webkit-appearance: none;
            -moz-appearance: none;
            appearance: none;
          }
        }
        &-select{
          width: 76px;
          margin-top: 8px;
          height: 45px;
          border-radius: 4px;
          border: 1px solid #ccc;
          background: #fff;
          line-height: 1.5;
          font-size: 16px;
        }
      }
      h3 {
        font-size: 16px;
        font-weight: bold;
      }
      .attention{
        margin: 8px 0 0;
      }
      .agree{
        text-align: center;
      }
      a {
        color: #0099e8;
        text-decoration: none;
      }
      span {
        margin: 0 2px;
      }
      .about{
        text-align: right;
        &__registered{
          color: #0099e8;
          text-decoration: none;
        }
        .fa-chevron-right {
          color: #0099e8;
        }
        .fa-question-circle {
          color: #0099e8;
          font-size: 1rem
        }
      }
    }
    .form-info-text {
      color: #888;
      margin-top: 8px;
      font-size: 14px;
    }
  }
  &__bottom {
    margin: 0 auto;
    max-width: 343px;
    .registance {
      text-align: right;
    }
  }
  .btn-default {
    width: 100%;
    height: 50px;
    background-color: $green;
    color: #FFFFFF;
    font-size: 15px;
    cursor: pointer;
  }
  .btn-registration{
    color: #fff;
    border-radius: 4px;
    width: 50%;
    line-height: 48px;
    border: 1px solid transparent;
    text-align: center;
    margin: 0 auto;
    position: relative;
    i{
      font-size:20px;
      position: absolute;
      top:13.5px;
      left:15px;
    }
    .about__registered {
      color: #FFFFFF;
      text-decoration: none;
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      cursor: pointer;
    }
    &.email{
      background-color: $green;
    }
    &.facebook{
      background-color:#385184;
    }
    &.google{
      background-color:#FFFFFF;
      color: black;
      background: #fff image-url('google.svg') 
      no-repeat 3px top;
      border: #979797 solid 1px;
    }
    &:not( :first-child ){
      margin-top: 16px;
    }
  }
}
new.html.haml
= render 'shared/main-header'
.container-fluid
  .row.py-5.w-100
    = render 'shared/mypage-side'
    .mypage-main.col-9
      %h2.header_title
        支払い方法
      .single-container
        .rgs-main__section
          = render "shared/creditcard-form"
        .payment-explain
          = fa_icon 'chevron-right', class:"arrows"
          = link_to '支払い方法について', '#'

モデル作成

ターミナル
$ rails g model creditcard

creditcardsテーブル

Column Type Options
user_id references foreign_key: true, null: false
payjp_id string null: false

Association

  • belongs_to :user

ということでマイグレーションファイルは下記になります。

migrationファイル
class CreateCreditcards < ActiveRecord::Migration[5.2]
  def change
    create_table :creditcards do |t|
      t.references :user,  foreign_key: true, null: false
      t.string :payjp_id, null: false
      t.timestamps
    end
  end
end

ここは他の記事と異なります。

  • user_id: AppのUser-ID
  • payjp_id: PAYJPのUser-ID

他の記事だとカード用のカラムも作成していますが、PAYJPのアカウントから引っ張りだせばいいので不要です。

本題のjQueryです。

Payjp.js
$(document).on('turbolinks:load',function(){
  // PAY.JPの公開鍵をセットします。
  Payjp.setPublicKey('pk_test_111111111111111111');

  //formのsubmitを止めるために, クレジットカード登録のformを定義します。
  var form = $(".form");

  $("#charge-form").click(function() {
    // submitが完了する前に、formを止めます。
    form.find("input[type=submit]").prop("disabled", true);
    // submitを止められたので、PAY.JPの登録に必要な処理をします。

    // formで入力された、カード情報を取得します。
    var card = {
      number: $("#card_number").val(),
      cvc: $("#cvc").val(),
      exp_month: $("#exp_month").val(),
      exp_year: $("#exp_year").val(),
    };

    // PAYJPに登録するためのトークン作成
    Payjp.createToken(card, function(status, response) {
      if (response.error){
        // エラーがある場合処理しない。
        form.find('.payment-errors').text(response.error.message);
        form.find('button').prop('disabled', false);
      }   
      else {
        // エラーなく問題なく進めた場合
        // formで取得したカード情報を削除して、Appにカード情報を残さない。
        $("#card_number").removeAttr("name");
        $("#cvc").removeAttr("name");
        $("#exp_month").removeAttr("name");
        $("#exp_year").removeAttr("name");
        var token = response.id;
        form.append($('<input type="hidden" name="payjpToken" />').val(token));
        form.get(0).submit();
      };
    });
  });
});

コントローラーの記述

コントローラー(new&create)の作成

creditcards_controller.rb
class CreditcardsController < ApplicationController
  require "payjp"
  before_action :set_card

  def new
    # cardがすでに登録済みの場合、indexのページに戻します。
    @card = Creditcard.where(user_id: current_user.id).first
    redirect_to action: "index" if @card.present?    
  end

  def create
    # PAY.JPの秘密鍵をセット(環境変数)
    Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]

    # jsで作成したpayjpTokenがちゃんと入っているか?
    if params['payjpToken'].blank?
      # トークンが空なら戻す
      render "new"
    else
      # トークンがちゃんとあれば進めて、PAY.JPに登録されるユーザーを作成します。
      customer = Payjp::Customer.create(
        description: 'test',
        email: current_user.email,
        card: params['payjpToken'],
        metadata: {user_id: current_user.id}
      )

      # PAY.JPのユーザーが作成できたので、creditcardモデルを登録します。
      @card = Creditcard.new(user_id: current_user.id, payjp_id: customer.id)
      if @card.save
        redirect_to action: "index", notice:"支払い情報の登録が完了しました"
      else
        render 'new'
      end
    end
  end

  private
  def set_card
    @card = Creditcard.where(user_id: current_user.id).first if Creditcard.where(user_id: current_user.id).present?
  end
end

コントローラーの追記(index、destory)

creditcards_controller.rb
class CreditcardsController < ApplicationController
  require "payjp"
  before_action :set_card

  def index
    # すでにクレジットカードが登録しているか?
    if @card.present?
      # 登録している場合,PAY.JPからカード情報を取得する
      # PAY.JPの秘密鍵をセットする。
      Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
      # PAY.JPから顧客情報を取得する。
      customer = Payjp::Customer.retrieve(@card.payjp_id)
      # PAY.JPの顧客情報から、デフォルトで使うクレジットカードを取得する。
      @card_info = customer.cards.retrieve(customer.default_card)
      # クレジットカード情報から表示させたい情報を定義する。
      # クレジットカードの画像を表示するために、カード会社を取得
      @card_brand = @card_info.brand
      # クレジットカードの有効期限を取得
      @exp_month = @card_info.exp_month.to_s
      @exp_year = @card_info.exp_year.to_s.slice(2,3) 

      # クレジットカード会社を取得したので、カード会社の画像をviewに表示させるため、ファイルを指定する。
      case @card_brand
      when "Visa"
        @card_image = "visa.svg"
      when "JCB"
        @card_image = "jcb.svg"
      when "MasterCard"
        @card_image = "master-card.svg"
      when "American Express"
        @card_image = "american_express.svg"
      when "Diners Club"
        @card_image = "dinersclub.svg"
      when "Discover"
        @card_image = "discover.svg"
      end
    end
  end

  def new
    @card = Creditcard.where(user_id: current_user.id).first
    redirect_to action: "index" if @card.present?    
  end

  def create
    Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
    if params['payjpToken'].blank?
      render "new"
    else
      customer = Payjp::Customer.create(
        description: 'test',
        email: current_user.email,
        card: params['payjpToken'],
        metadata: {user_id: current_user.id}
      )
      @card = Creditcard.new(user_id: current_user.id, payjp_id: customer.id)
      if @card.save
        if request.referer&.include?("/registrations/step5")
          redirect_to controller: 'registrations', action: "step6"
        else
          redirect_to action: "index", notice:"支払い情報の登録が完了しました"
        end
      else
        render 'new'
      end
    end
  end

  def destroy     
    # 今回はクレジットカードを削除するだけでなく、PAY.JPの顧客情報も削除する。これによりcreateメソッドが複雑にならない。
    # PAY.JPの秘密鍵をセットして、PAY.JPから情報をする。
    Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
    # PAY.JPの顧客情報を取得
    customer = Payjp::Customer.retrieve(@card.payjp_id)
    customer.delete # PAY.JPの顧客情報を削除
    if @card.destroy # App上でもクレジットカードを削除
      redirect_to action: "index", notice: "削除しました"
    else
      redirect_to action: "index", alert: "削除できませんでした"
    end
  end

  private
  def set_card
    @card = Creditcard.where(user_id: current_user.id).first if Creditcard.where(user_id: current_user.id).present?
  end
end

登録したカード情報を表示

Image from Gyazo
index.html.hamlに記述します。

index.html.haml
.mypage-main.col-9
      %h2.header_title
        支払い方法
      .single-container
        %section.creditcard_section
          %h3 クレジットカード一覧
          - if @card.present?
            .container
              .creditcard-info
                = image_tag "creditcards/#{@card_image}",width:'34',height:'20', alt:'master-card'
                %p.creditcard-info__number
                  = "**** **** **** " + @card_info.last4 #クレジットカードの下4桁を表示
                %p.creditcard-info__period 
                = @exp_month + " / " + @exp_year
                = button_to "削除する", creditcard_path(@card), method: :delete, class:"creditcard-info__delete"
          - else
            .new-card
              = link_to new_creditcard_path, class:"new-card-btn" do
                %i.far.fa-credit-card 
                クレジットカードを追加する

クレジットカードがない場合は下記の表示にさせます
Image from Gyazo

購入処理を追加

creditcards_controller.rb
def buy
    @product = Product.find(params[:product_id])
    # すでに購入されていないか?
    if @product.buyer.present? 
      redirect_back(fallback_location: root_path) 
    elsif @card.blank?
      # カード情報がなければ、買えないから戻す
      redirect_to action: "new"
      flash[:alert] = '購入にはクレジットカード登録が必要です'
    else
      # 購入者もいないし、クレジットカードもあるし、決済処理に移行
      Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
      # 請求を発行
      Payjp::Charge.create(
      amount: @product.price,
      customer: @card.customer_id,
      currency: 'jpy',
      )
      # 売り切れなので、productの情報をアップデートして売り切れにします。
      if @product.update(buyer_id: current_user.id)
        flash[:notice] = '購入しました。'
        redirect_to controller: 'products', action: 'show', id: @product.id
      else
        flash[:alert] = '購入に失敗しました。'
        redirect_to controller: 'products', action: 'show', id: @product.id
      end
    end
  end

あとはボタンを押したら、buyアクションが動くようにすれば、完了です!!

以上です
お疲れ様です。

参考リンク

新規登録時にクレジットカード登録

購入処理

テストカード

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

【Ruby on Rails】gem 'ransack' を利用した検索機能/フォームの実装

検索機能をgemを使って簡単に実装しましょう!

gem 'ransack'を利用した検索機能実装

Image from Gyazo

インストール

Gemfile
gem 'ransack'

インストールしましょう

ターミナル
$ bundle install

検索機能の作成

application_controller.rb
before_action :set_search

def set_search
  @search = Product.ransack(params[:q]) #ransackの検索メソッド
  @search_products = @search.result(distinct: true).order(created_at: "DESC").includes(:user).page(params[:page]).per(5) # productsの検索結果一覧 
   # 最終的に、@search_productsを検索結果画面(例:search.html.haml)に挿入します。
   # 検索結果の一覧:  @search_products = @search.result.order(created_at: "DESC")
   # distinct: trueは検索結果のレコード重複しないようにします。
   # ページネーション:  .includes(:user).page(params[:page]).per(5
end

application_controller.rbで定義します。

products_controller.rbに書くとproducts_controller.rbを使わないページを表示した時にエラーになるため、application_controller.rbで記述。
kaminariでページネーションをつけているのでpage(params[:page])をつけた。
searchメソッドよりransackメソッド推奨なのでransackメソッドを使った。

ransackの検索オプション

検索方法 意味(英語) 意味
*_eq equal 等しい
*noteq not equal 等しくない
*_lt less than より小さい
*_lteq less than or equal より小さい(等しいものも含む)
*_gt grater than より大きい
*_gteq grater than or equal より大きい(等しいものも含む)
*_cont contains value 部分一致(内容を含む)

今回使ったのは*_contですね

distinct: trueの参考になるページ

検索フォームの作成

検索.html.haml
.search_wap
   = search_form_for @search do |f|
     = f.text_field :name_cont , placeholder: "何かお探しですか?", class: 'input'
       = button_tag type: 'submit', class: 'search__button' do
         .icon_wap
           %i.fas.fa-search

haml形式でフォームを作成した。
この段階では、url:〇〇_pathはつけていません。あとで付与していきます。
scssのmixinも記載しておきます。

mixin_search
@mixin input{
  border: none;
  border-radius: 50px;
  background: #F4F8F9;
  height: 37px;
  width: 100%;
  padding: 10px 20px;
}

.search_wap{
  margin-right: 15px;
  position: relative; 
  .input{
    @include input;
    margin: 13px 0 0;
  }
  .search__button{
    background-color: transparent;
    cursor: pointer;
    position: absolute;
    right: 0px;
    top: 13px;
    height: 37px;
    width: 40px;
    border-style: none;
    border-radius: 0px 4px 4px 0px;
    padding: 0px;
    .icon_wap{
      i{
        position: absolute;
        right: 14px;
        top: 13px;
        font-size: 13px;
        font-weight: 900;
        color: #999999;
      }
    }
  }
}

検索結果のview作成

あとは、検索画面を作成して、検索結果である@search_productsを挿入してあげれば完成です。

検索結果用の画面に移行するために、route.rbにget :searchを追加します。

route.rb
Rails.application.routes.draw do
  resources :products do
    collection do
      get :search
    end
  end
end

では、検索フォームにurlを追加しましょう

検索.html.haml
.search_wap
   = search_form_for @search, url: search_users_path do |f|
     = f.text_field :name_cont , placeholder: "何かお探しですか?", class: 'input'
       = button_tag type: 'submit', class: 'search__button' do
         .icon_wap
           %i.fas.fa-search

view用のhtml.hmlとコントローラーを記述します。

products_controller.rb
class UsersController < ApplicationController
  def search
  end
end

search.html.haml
= render 'shared/main-header'
.container-fluid
  .row.py-5.w-100
    .jscroll
      = render partial: 'user', collection: @search_products, as: "product", class: "jscroll"

// @search_productはapplication_controller.rbで定義している。

@search_productで検索結果のProductを表示させるので、indexで作成したviewに@products@search_productに書き換えれば、出来上がりです。

まとめ

検索処理は下記だけ

application_controller.rb
before_action :set_search
def set_search
  @search = Product.ransack(params[:q]) #ransackの検索処理
  @search_products = @search.result # 検索結果
end

検索フォームを作って

検索.html.haml
.search_wap
   = search_form_for @search do |f|
     = f.text_field :name_cont , placeholder: "何かお探しですか?", class: 'input'
       = button_tag type: 'submit', class: 'search__button' do
         .icon_wap
           %i.fas.fa-search

あとはviewに@search_productsを挿入すれば完成。

if文を追加する

if文を追加するなら、下記の3パターンがいいかもしません

// 検索キーワードなし: 空白だとスペースを入れいてるname全部がヒットしてしまうため
- if params[:q]['name_cont'] == ""
  = "検索キーワードがありません。"

// 検索結果ありの場合
- elsif @search_products.present?
  = "「#{params[:q][:name_cont]}」の検索結果: #{@search_products.count}個"

// 検索数0の場合
- else
  = "検索に一致する商品はありませんでした"

では実際に書いていきましょう!

参考リンク

ransackでRailsアプリのヘッダーに検索機能をつける
【Rails】ヘッダーへの検索機能の付け方
Rubyon Rails で検索機能を作ろう(ransack)
[Rails]ransackを利用した色々な検索フォーム作成方法まとめ
Rails 5.1とBootstrapで作るシンプルな検索機能のテンプレ

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

Railsを用いたアプリケーション開発の流れ

はじめに

Railsを用いてアプリケーション開発をするための流れを簡潔にまとめたいと思います。

概要

以下の流れで実装を行い、トップページにデータベースに保存してあるデータを表示する機能を作成します。
1.アプリケーションの土台となるものを作る
2.データベースを作る
3.ルーティングの設定
4.コントローラの作成
5.ビューの作成
6.モデルの作成
7.ビューにインスタンス変数を埋め込む

1.アプリケーションの土台となるものを作る

まずはアプリケーション用のディレクトリを作成してください。
そのディレクトリの直下で以下のコマンドを入力します。x.x.xの部分はrailsのバージョンを入れてください。
rails _x.x.x_ new アプリケーション名 -d 使用するデータベース
これでコマンドを実行したディレクトリの直下に様々なファイルが作成されたはずです。これがアプリケーションの土台となります。

2.データベースを作る

以下のコマンドでアプリケーションで使用するデータベースを作成する事ができます。
rails db:create
実際にデータベースを確認して見ましょう。「アプリケーション名_development」と「アプリケーション名_test」というデータベースが作成されているはずです。

3.ルーティングの設定

「1.アプリケーションの土台となるものを作る」を実行していれば、/アプリケーション用のディレクトリ/config/app の配下にroutes.rbというファイルが作成されていると思います。このファイルに「root to: "posts#index"」を追記しましょう。

Rails.application.routes.draw do
  root to: "posts#index"
end

これで「root」ディレクトリにリクエストがあった場合、
「posts」コントローラに処理が遷移し、
postsコントローラの「index」アクションの処理を行う、となります。

4.コントローラの作成

以下のコマンドでコントローラーを作成できます。「3.ルーティングの設定」でpostsコントローラに処理が遷移するとしたため、今回はコントローラー名をpostsにしましょう。
rails g controller コントローラー名

5.ビューの作成

今回は、postsコントローラのindexアクションに対応するビューであるため、/アプリケーション用のディレクトリ/app/views/posts の配下にindex.html.erbを作成しましょう。コントローラから受け取るインデックス変数などの処理は後ほど記載します。

6.モデルの作成

ターミナルで以下のコマンドを実行しましょう。
これでモデル名をpostにすればpostsテーブルに対応するモデルができます。
rails g model モデル名

7.ビューにインスタンス変数を埋め込む

1~6までの流れでテーブルからデータを抽出する事ができ、@posts(インスタンス変数)にデータが格納されました。あとはビューを修正してインスタンス変数を画面に出力するようにすれば完成です。

# 修正例
<% @posts.each do |post|%>
  <div>
    <%= post.text %>
  </div>
<% end %>

雑感

3日ほどRubyとRailsについて学び、WEBアプリケーション制作におけるはじめの一歩が踏み出せたのではないかと思います。ちなみに筆者は漫画のはじめの一歩も大好きで、ベストバウトは間柴対木村です。

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

Devise4.7をRails6+ruby 2.7.0で動かす

はじめに

さっき、「Devise4.7をRails6で動かす」を書いたばかりなのにRuby 2.7.0が使えることに気づき、やり直した。

1. ruby 2.7.0 install

2.7.0があるか確認するとちゃんとあった。

$ rbenv install -l | grep ^2
途中省略
2.7.0-dev
2.7.0-preview1
2.7.0-preview2
2.7.0-preview3
2.7.0-rc1
2.7.0-rc2
2.7.0
2.8.0-dev
$ rbenv install 2.7.0
Downloading openssl-1.1.1d.tar.gz...
-> https://dqw8nmjcqpjn7.cloudfront.net/1e3a91bc1f9dfce01af26026f856e064eab4c8ee0a8f457b5ae30b40b8b711f2
Installing openssl-1.1.1d...
Installed openssl-1.1.1d to /Users/you/.rbenv/versions/2.7.0

Downloading ruby-2.7.0.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.0.tar.bz2
Installing ruby-2.7.0...
ruby-build: using readline from homebrew
Installed ruby-2.7.0 to /Users/you/.rbenv/versions/2.7.0

Communized gems for 2.7.0
/usr/local/bin/rbenv-communal-gem-home: line 21: /usr/local/bin/../version_cache/2.7.0: Permission denied

なぜPermission denied? 調べるのは面倒なのでsudoで再実行。

$ sudo rbenv install 2.7.0
rbenv: /Users/you/.rbenv/versions/2.7.0 already exists
continue with installation? (y/N) y
Downloading openssl-1.1.1d.tar.gz...
-> https://dqw8nmjcqpjn7.cloudfront.net/1e3a91bc1f9dfce01af26026f856e064eab4c8ee0a8f457b5ae30b40b8b711f2
Installing openssl-1.1.1d...
Installed openssl-1.1.1d to /Users/you/.rbenv/versions/2.7.0

Downloading ruby-2.7.0.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.0.tar.bz2
Installing ruby-2.7.0...
ruby-build: using readline from homebrew
Installed ruby-2.7.0 to /Users/you/.rbenv/versions/2.7.0

Communized gems for 2.7.0
$ rbenv versions
  system
  2.3.3
  2.5.1
* 2.5.3 (set by /Users/your/apps/devise-rails6/.ruby-version)
  2.6.5
  2.7.0 <=ちゃんとインストールされている。
  2.7.0-rc1

Rubyのバージョンごとにgemをインストールしないといけないらしい。

$ gem install bundler
Successfully installed bundler-2.1.2
Parsing documentation for bundler-2.1.2
Installing ri documentation for bundler-2.1.2
Done installing documentation for bundler after 2 seconds
1 gem installed

2. Rails newでプロジェクト作成

rbenv rehashでRubyMineが読み取れるようにインストールしたRubyを反映させておきます。それからRubyMineでプロジェクトを作成します。

スクリーンショット 2020-01-01 午前3.00.10.png

ここでいろいろエラーが出て、解決するまで時間を使ったが、Railsの問題は本記事のDeviseとは関係ないので割愛する。

ところが、次のエラーが出た。

An error occurred while installing mysql2 (0.5.3), and Bundler cannot continue.
Make sure that `gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'` succeeds before bundling.

素直にgem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'を実行したが、またエラー。

$ gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'
Building native extensions. This could take a while...
ERROR:  Error installing mysql2:
    ERROR: Failed to build gem native extension.

途中省略

linking shared-object mysql2/mysql2.bundle
ld: library not found for -lssl <=これが問題だ。
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [mysql2.bundle] Error 1

make failed, exit code 2

参考記事 の下の方に下記のヒントがあった。

$> brew install mysql@5.6
$> bundle config build.mysql2 --with-mysql-config=/usr/local/Cellar/mysql\@5.6/5.6.42/bin/mysql_config
$> bundle install

私はMySQLのバージョンはいじりたくない(他のアプリに影響が出るのを避けたい)し、MySQLは5.7で古くないので、自分のMacのMySQL5.7に合わせて次を実行した。

$ bundle config build.mysql2 --with-mysql-config=/usr/local/Cellar/mysql@5.7/5.7.24/bin/mysql_config
$ sudo bundle install

これでbundle installは成功した。

参考記事

mysql2 gem fails to compile with MySQL 5.6.12 on OS X with Homebrew

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

mac rbenv バージョンの違うRubyをインストールして切り替えて使用する

目的

  • home brewを用いてmacにバージョン違いのRubyをインストールする方法をまとめる

実施方法概要

  1. home brewのインストールリストの更新
  2. Rubyのインストール
  3. Rubyバージョンの切り替え

実施方法詳細

  1. home brewのインストールリストの更新

    1. 下記コマンドを実行してhome brewのインストールリストを更新する。

      $ brew upgrade ruby-build
      
  2. Rubyのインストール

    1. 下記コマンドを実行して欲しいバージョンのRubyをrbenv経由でインストールする。(X.X.Xがバージョン)

      $ rbenv install X.X.X
      
  3. インストール確認

    1. 下記コマンドを実行して先に指定したバージョンのRubyがインストールされていることを確認する。(*が付いているものが現在割り当てられているバージョンになる)

      $ rbenv versions
        system
        2.3.0
        * 2.5.0 (set by /Users/shun/workspace/study/rails/tropical_fish_sns_of_rails/.ruby-version)
        2.6.2
      
  4. Rubyバージョンの切り替え

    1. 下記コマンドを実行してインストールされているRubyのバージョンを変更する。(X.X.Xはバージョン、$ rbenv versionsの出力の中から指定する。)

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

Rails新規アプリケーションの作り方

フレームワークRailsの作成手順(ほぼ備忘録)

ディレクトリ作成からRailsのひな形作成までを簡単にアウトプット練習も兼ねて書いていく。(DB設計は割愛)

開発環境↓

  • Railsバージョン5.2.3
  • データベース MySQL

ここからはターミナルを実行してひな形を作っていく。

ターミナル
# ディレクトリ作成
$ mkdir ~/test 

Rails newコマンドで作成

ターミナル
$ rails new アプリケーション名 -オプション
test
#test-appファイルの作成
$ rails _5.2.3_ new test-app -d mysql

$ cd test-apptest-appファイルに移動

test-app
# bundle installしておく
$ bundle install

これでRailsフレームワークのひな形が完成。

htmlファイルやcssファイル、routes.rbがあることを確認しておく。

これからDBとモデルを作成する。

以上

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

[Rails] ActiveRecord::import で NoMethodError: undefined method `raise_record_invalid' が発生したときの対応

はじめに

ActiveRecord を使ってDBアクセスをしている場合、バルクインサートをやってくれる gem に activerecord-import というのがある。
とても便利なライブラリなのだが、RSpec で ActiveRecord::RecordInvalid の発生を期待するテストを書いた際に当該エラーが発生しなかったので備忘録として残す。

環境

バージョン 備考
Ruby v2.5.5p157
Ruby on Rails v5.2.3
activerecord-import v0.14.1 バルクインサートをやってくれる

なにが発生したか

前述のとおり ActiveRecord::RecordInvalid が発生するテストを書いたのだが、次のエラーが発生した。

expected ActiveRecord::RecordInvalid, got #<NoMethodError: undefined method `raise_record_invalid' for

結論から

この現象の解決には activerecord-import の update を行う。( 下記のバージョンを指定して bundle install を実行する )

gem 'activerecord-import', '~> 0.15.0'

発生していた理由

activerecord-import のバグ。下記で fix の報告があった。

https://github.com/zdennis/activerecord-import/pull/294

まとめにかえて

上記のプルリクエストがマージされたのは on 3 Jul 2016 と古い。

スクリーンショット 2020-02-21 12.22.46.png

そういうわけなので、最近導入した方は本記事で取り上げた現象は発生していないと思われるが、導入したのが古いバージョンのままで更新をかけていないとハマるのでご注意を。

なんせ ActiveRecord::import, ActiveRecord::RecordInvalid, NoMethodError, undefined method raise_record_invalid といったキーワードで検索かけても全然糸口が見つからない。
そんななか、検索でひっかかった こちら前掲のプルリクエストがあって気づけたのは幸いでした。。。

参考

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